Index: ps/trunk/source/ps/CLogger.cpp =================================================================== --- ps/trunk/source/ps/CLogger.cpp (revision 23320) +++ ps/trunk/source/ps/CLogger.cpp (revision 23321) @@ -1,341 +1,342 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "CLogger.h" #include "graphics/FontMetrics.h" #include "graphics/ShaderManager.h" #include "graphics/TextRenderer.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/utf8.h" #include "ps/CConsole.h" #include "ps/Profile.h" +#include "ps/Pyrogenesis.h" #include "renderer/Renderer.h" #include #include #include CStrW g_UniqueLogPostfix; static const double RENDER_TIMEOUT = 10.0; // seconds before messages are deleted static const double RENDER_TIMEOUT_RATE = 10.0; // number of timed-out messages deleted per second static const size_t RENDER_LIMIT = 20; // maximum messages on screen at once // Set up a default logger that throws everything away, because that's // better than crashing. (This is particularly useful for unit tests which // don't care about any log output.) struct BlackHoleStreamBuf : public std::streambuf { } blackHoleStreamBuf; std::ostream blackHoleStream(&blackHoleStreamBuf); CLogger nullLogger(&blackHoleStream, &blackHoleStream, false, true); CLogger* g_Logger = &nullLogger; const char* html_header0 = "\n" "\n" "Pyrogenesis Log\n" "\n" "

0 A.D. ("; const char* html_header1 = "

\n"; CLogger::CLogger() { OsPath mainlogPath(psLogDir() / (L"mainlog" + g_UniqueLogPostfix + L".html")); m_MainLog = new std::ofstream(OsString(mainlogPath).c_str(), std::ofstream::out | std::ofstream::trunc); debug_printf("Writing the mainlog at %s\n", mainlogPath.string8().c_str()); OsPath interestinglogPath(psLogDir() / (L"interestinglog" + g_UniqueLogPostfix + L".html")); m_InterestingLog = new std::ofstream(OsString(interestinglogPath).c_str(), std::ofstream::out | std::ofstream::trunc); m_OwnsStreams = true; m_UseDebugPrintf = true; Init(); } CLogger::CLogger(std::ostream* mainLog, std::ostream* interestingLog, bool takeOwnership, bool useDebugPrintf) { m_MainLog = mainLog; m_InterestingLog = interestingLog; m_OwnsStreams = takeOwnership; m_UseDebugPrintf = useDebugPrintf; Init(); } void CLogger::Init() { m_RenderLastEraseTime = -1.0; // this is called too early to allow us to call timer_Time(), // so we'll fill in the initial value later m_NumberOfMessages = 0; m_NumberOfErrors = 0; m_NumberOfWarnings = 0; *m_MainLog << html_header0 << engine_version << ") Main log" << html_header1; *m_InterestingLog << html_header0 << engine_version << ") Main log (warnings and errors only)" << html_header1; } CLogger::~CLogger() { char buffer[128]; sprintf_s(buffer, ARRAY_SIZE(buffer), " with %d message(s), %d error(s) and %d warning(s).", m_NumberOfMessages,m_NumberOfErrors,m_NumberOfWarnings); time_t t = time(NULL); struct tm* now = localtime(&t); char currentDate[17]; sprintf_s(currentDate, ARRAY_SIZE(currentDate), "%04d-%02d-%02d", 1900+now->tm_year, 1+now->tm_mon, now->tm_mday); char currentTime[10]; sprintf_s(currentTime, ARRAY_SIZE(currentTime), "%02d:%02d:%02d", now->tm_hour, now->tm_min, now->tm_sec); //Write closing text *m_MainLog << "

Engine exited successfully on " << currentDate; *m_MainLog << " at " << currentTime << buffer << "

\n"; *m_InterestingLog << "

Engine exited successfully on " << currentDate; *m_InterestingLog << " at " << currentTime << buffer << "

\n"; if (m_OwnsStreams) { SAFE_DELETE(m_InterestingLog); SAFE_DELETE(m_MainLog); } } static std::string ToHTML(const char* message) { std::string cmessage = message; boost::algorithm::replace_all(cmessage, "&", "&"); boost::algorithm::replace_all(cmessage, "<", "<"); return cmessage; } void CLogger::WriteMessage(const char* message, bool doRender = false) { std::string cmessage = ToHTML(message); std::lock_guard lock(m_Mutex); ++m_NumberOfMessages; // if (m_UseDebugPrintf) // debug_printf("MESSAGE: %s\n", message); *m_MainLog << "

" << cmessage << "

\n"; m_MainLog->flush(); if (doRender) { if (g_Console) g_Console->InsertMessage(std::string("INFO: ") + message); PushRenderMessage(Normal, message); } } void CLogger::WriteError(const char* message) { std::string cmessage = ToHTML(message); std::lock_guard lock(m_Mutex); ++m_NumberOfErrors; if (m_UseDebugPrintf) debug_printf("ERROR: %.16000s\n", message); if (g_Console) g_Console->InsertMessage(std::string("ERROR: ") + message); *m_InterestingLog << "

ERROR: " << cmessage << "

\n"; m_InterestingLog->flush(); *m_MainLog << "

ERROR: " << cmessage << "

\n"; m_MainLog->flush(); PushRenderMessage(Error, message); } void CLogger::WriteWarning(const char* message) { std::string cmessage = ToHTML(message); std::lock_guard lock(m_Mutex); ++m_NumberOfWarnings; if (m_UseDebugPrintf) debug_printf("WARNING: %s\n", message); if (g_Console) g_Console->InsertMessage(std::string("WARNING: ") + message); *m_InterestingLog << "

WARNING: " << cmessage << "

\n"; m_InterestingLog->flush(); *m_MainLog << "

WARNING: " << cmessage << "

\n"; m_MainLog->flush(); PushRenderMessage(Warning, message); } void CLogger::Render() { PROFILE3_GPU("logger"); CleanupRenderQueue(); CStrIntern font_name("mono-stroke-10"); CFontMetrics font(font_name); int lineSpacing = font.GetLineSpacing(); CShaderTechniquePtr textTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text); textTech->BeginPass(); CTextRenderer textRenderer(textTech->GetShader()); textRenderer.Font(font_name); textRenderer.Color(1.0f, 1.0f, 1.0f); // Offset by an extra 35px vertically to avoid the top bar. textRenderer.Translate(4.0f, 35.0f + lineSpacing, 0.0f); // (Lock must come after loading the CFont, since that might log error messages // and attempt to lock the mutex recursively which is forbidden) std::lock_guard lock(m_Mutex); for (const RenderedMessage& msg : m_RenderMessages) { const char* type; if (msg.method == Normal) { type = "info"; textRenderer.Color(0.0f, 0.8f, 0.0f); } else if (msg.method == Warning) { type = "warning"; textRenderer.Color(1.0f, 1.0f, 0.0f); } else { type = "error"; textRenderer.Color(1.0f, 0.0f, 0.0f); } CMatrix3D savedTransform = textRenderer.GetTransform(); textRenderer.PrintfAdvance(L"[%8.3f] %hs: ", msg.time, type); // Display the actual message in white so it's more readable textRenderer.Color(1.0f, 1.0f, 1.0f); textRenderer.Put(0.0f, 0.0f, msg.message.c_str()); textRenderer.SetTransform(savedTransform); textRenderer.Translate(0.0f, (float)lineSpacing, 0.0f); } textRenderer.Render(); textTech->EndPass(); } void CLogger::PushRenderMessage(ELogMethod method, const char* message) { double now = timer_Time(); // Add each message line separately const char* pos = message; const char* eol; while ((eol = strchr(pos, '\n')) != NULL) { if (eol != pos) { RenderedMessage r = { method, now, std::string(pos, eol) }; m_RenderMessages.push_back(r); } pos = eol + 1; } // Add the last line, if we didn't end on a \n if (*pos != '\0') { RenderedMessage r = { method, now, std::string(pos) }; m_RenderMessages.push_back(r); } } void CLogger::CleanupRenderQueue() { std::lock_guard lock(m_Mutex); if (m_RenderMessages.empty()) return; double now = timer_Time(); // Initialise the timer on the first call (since we can't do it in the ctor) if (m_RenderLastEraseTime == -1.0) m_RenderLastEraseTime = now; // Delete old messages, approximately at the given rate limit (and at most one per frame) if (now - m_RenderLastEraseTime > 1.0/RENDER_TIMEOUT_RATE) { if (m_RenderMessages[0].time + RENDER_TIMEOUT < now) { m_RenderMessages.pop_front(); m_RenderLastEraseTime = now; } } // If there's still too many then delete the oldest if (m_RenderMessages.size() > RENDER_LIMIT) m_RenderMessages.erase(m_RenderMessages.begin(), m_RenderMessages.end() - RENDER_LIMIT); } TestLogger::TestLogger() { m_OldLogger = g_Logger; g_Logger = new CLogger(&m_Stream, &blackHoleStream, false, false); } TestLogger::~TestLogger() { delete g_Logger; g_Logger = m_OldLogger; } std::string TestLogger::GetOutput() { return m_Stream.str(); } TestStdoutLogger::TestStdoutLogger() { m_OldLogger = g_Logger; g_Logger = new CLogger(&std::cout, &blackHoleStream, false, false); } TestStdoutLogger::~TestStdoutLogger() { delete g_Logger; g_Logger = m_OldLogger; } Index: ps/trunk/source/ps/Mod.cpp =================================================================== --- ps/trunk/source/ps/Mod.cpp (revision 23320) +++ ps/trunk/source/ps/Mod.cpp (revision 23321) @@ -1,158 +1,159 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ps/Mod.h" #include #include "lib/file/file_system.h" #include "lib/file/vfs/vfs.h" #include "lib/utf8.h" #include "ps/Filesystem.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" +#include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptRuntime.h" std::vector g_modsLoaded; std::vector> g_LoadedModVersions; CmdLineArgs g_args; JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedObject obj(cx, JS_NewPlainObject(cx)); const Paths paths(g_args); // loop over all possible paths OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; DirectoryNames modDirs; DirectoryNames modDirsUser; GetDirectoryEntries(modPath, NULL, &modDirs); // Sort modDirs so that we can do a fast lookup below std::sort(modDirs.begin(), modDirs.end()); PIVFS vfs = CreateVfs(); for (DirectoryNames::iterator iter = modDirs.begin(); iter != modDirs.end(); ++iter) { vfs->Clear(); if (vfs->Mount(L"", modPath / *iter, VFS_MOUNT_MUST_EXIST) < 0) continue; CVFSFile modinfo; if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK) continue; JS::RootedValue json(cx); if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json)) continue; // Valid mod, add it to our structure JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json); } GetDirectoryEntries(modUserPath, NULL, &modDirsUser); bool dev = InDevelopmentCopy(); for (DirectoryNames::iterator iter = modDirsUser.begin(); iter != modDirsUser.end(); ++iter) { // If we are in a dev copy we do not mount mods in the user mod folder that // are already present in the mod folder, thus we skip those here. if (dev && std::binary_search(modDirs.begin(), modDirs.end(), *iter)) continue; vfs->Clear(); if (vfs->Mount(L"", modUserPath / *iter, VFS_MOUNT_MUST_EXIST) < 0) continue; CVFSFile modinfo; if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK) continue; JS::RootedValue json(cx); if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json)) continue; // Valid mod, add it to our structure JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json); } return JS::ObjectValue(*obj); } void Mod::CacheEnabledModVersions(const shared_ptr& scriptRuntime) { ScriptInterface scriptInterface("Engine", "CacheEnabledModVersions", scriptRuntime); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue availableMods(cx, GetAvailableMods(scriptInterface)); g_LoadedModVersions.clear(); for (const CStr& mod : g_modsLoaded) { // Ignore user and mod mod as they are irrelevant for compatibility checks if (mod == "mod" || mod == "user") continue; CStr version; JS::RootedValue modData(cx); if (scriptInterface.GetProperty(availableMods, mod.c_str(), &modData)) scriptInterface.GetProperty(modData, "version", version); g_LoadedModVersions.push_back({mod, version}); } } JS::Value Mod::GetLoadedModsWithVersions(const ScriptInterface& scriptInterface) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue returnValue(cx); scriptInterface.ToJSVal(cx, &returnValue, g_LoadedModVersions); return returnValue; } JS::Value Mod::GetEngineInfo(const ScriptInterface& scriptInterface) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue mods(cx, Mod::GetLoadedModsWithVersions(scriptInterface)); JS::RootedValue metainfo(cx); ScriptInterface::CreateObject( cx, &metainfo, "engine_version", engine_version, "mods", mods); scriptInterface.FreezeObject(metainfo, true); return metainfo; } Index: ps/trunk/source/ps/ModIo.h =================================================================== --- ps/trunk/source/ps/ModIo.h (revision 23320) +++ ps/trunk/source/ps/ModIo.h (revision 23321) @@ -1,208 +1,209 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef INCLUDED_MODIO #define INCLUDED_MODIO #include "lib/external_libraries/curl.h" +#include "lib/os_path.h" #include "scriptinterface/ScriptInterface.h" #include #include // TODO: Allocate instance of the below two using sodium_malloc? struct PKStruct { unsigned char sig_alg[2] = {}; // == "Ed" unsigned char keynum[8] = {}; // should match the keynum in the sigstruct, else this is the wrong key unsigned char pk[crypto_sign_PUBLICKEYBYTES] = {}; }; struct SigStruct { unsigned char sig_alg[2] = {}; // "ED" (since we only support the hashed mode) unsigned char keynum[8] = {}; // should match the keynum in the PKStruct unsigned char sig[crypto_sign_BYTES] = {}; }; struct ModIoModData { std::map properties; std::vector dependencies; SigStruct sig; }; enum class DownloadProgressStatus { NONE, // Default state GAMEID, // The game ID is being downloaded READY, // The game ID has been downloaded LISTING, // The mod list is being downloaded LISTED, // The mod list has been downloaded DOWNLOADING, // A mod file is being downloaded SUCCESS, // A mod file has been downloaded FAILED_GAMEID, // Game ID couldn't be retrieved FAILED_LISTING, // Mod list couldn't be retrieved FAILED_DOWNLOADING, // File couldn't be retrieved FAILED_FILECHECK // The file is corrupted }; struct DownloadProgressData { DownloadProgressStatus status; double progress; std::string error; }; struct DownloadCallbackData; /** * mod.io API interfacing code. * * Overview * * This class interfaces with a remote API provider that returns a list of mod files. * These can then be downloaded after some cursory checking of well-formedness of the returned * metadata. * Downloaded files are checked for well formedness by validating that they fit the size and hash * indicated by the API, then we check if the file is actually signed by a trusted key, and only * if all of that is success the file is actually possible to be loaded as a mod. * * Security considerations * * This both distrusts the loaded JS mods, and the API as much as possible. * We do not want a malicious mod to use this to download arbitrary files, nor do we want the API * to make us download something we have not verified. * Therefore we only allow mods to download one of the mods returned by this class (using indices). * * This (mostly) necessitates parsing the API responses here, as opposed to in JS. * One could alternatively parse the responses in a locked down JS context, but that would require * storing that code in here, or making sure nobody can overwrite it. Also this would possibly make * some of the needed accesses for downloading and verifying files a bit more complicated. * * Everything downloaded from the API has its signature verified against our public key. * This is a requirement, as otherwise a compromise of the API would result in users installing * possibly malicious files. * So a compromised API can just serve old files that we signed, so in that case there would need * to be an issue in that old file that was missed. * * To limit the extend to how old those files could be the signing key should be rotated * regularly (e.g. every release). To allow old versions of the engine to still use the API * files can be signed by both the old and the new key for some amount of time, that however * only makes sense in case a mod is compatible with both engine versions. * * Note that this does not prevent all possible attacks a package manager/update system should * defend against. This is intentionally not an update system since proper package managers already * exist. However there is some possible overlap in attack vectors and these should be evalutated * whether they apply and to what extend we can fix that on our side (or how to get the API provider * to help us do so). For a list of some possible issues see: * https://github.com/theupdateframework/specification/blob/master/tuf-spec.md * * The mod.io settings are also locked down such that only mods that have been authorized by us * show up in API queries. This is both done so that all required information (dependencies) * are stored for the files, and that only mods that have been checked for being ok are actually * shown to users. */ class ModIo { NONCOPYABLE(ModIo); public: ModIo(); ~ModIo(); // Async requests void StartGetGameId(); void StartListMods(); void StartDownloadMod(size_t idx); /** * Advance the current async request and perform final steps if the download is complete. * * @param scriptInterface used for parsing the data and possibly install the mod. * @return true if the download is complete (successful or not), false otherwise. */ bool AdvanceRequest(const ScriptInterface& scriptInterface); /** * Cancel the current async request and clean things up */ void CancelRequest(); const std::vector& GetMods() const { return m_ModData; } const DownloadProgressData& GetDownloadProgress() const { return m_DownloadProgressData; } private: static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp); static size_t DownloadCallback(void* buffer, size_t size, size_t nmemb, void* userp); static int DownloadProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow); CURLMcode SetupRequest(const std::string& url, bool fileDownload); void TearDownRequest(); bool ParseGameId(const ScriptInterface& scriptInterface, std::string& err); bool ParseMods(const ScriptInterface& scriptInterface, std::string& err); void DeleteDownloadedFile(); bool VerifyDownloadedFile(std::string& err); // Utility methods for parsing mod.io responses and metadata static bool ParseGameIdResponse(const ScriptInterface& scriptInterface, const std::string& responseData, int& id, std::string& err); static bool ParseModsResponse(const ScriptInterface& scriptInterface, const std::string& responseData, std::vector& modData, const PKStruct& pk, std::string& err); static bool ParseSignature(const std::vector& minisigs, SigStruct& sig, const PKStruct& pk, std::string& err); // Url parts std::string m_BaseUrl; std::string m_GamesRequest; std::string m_GameId; // Query parameters std::string m_ApiKey; std::string m_IdQuery; CURL* m_Curl; CURLM* m_CurlMulti; curl_slist* m_Headers; char m_ErrorBuffer[CURL_ERROR_SIZE]; std::string m_ResponseData; // Current mod download int m_DownloadModID; OsPath m_DownloadFilePath; DownloadCallbackData* m_CallbackData; DownloadProgressData m_DownloadProgressData; PKStruct m_pk; std::vector m_ModData; friend class TestModIo; }; extern ModIo* g_ModIo; #endif // INCLUDED_MODIO Index: ps/trunk/source/ps/ProfileViewer.cpp =================================================================== --- ps/trunk/source/ps/ProfileViewer.cpp (revision 23320) +++ ps/trunk/source/ps/ProfileViewer.cpp (revision 23321) @@ -1,624 +1,625 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * Implementation of profile display (containing only display routines, * the data model(s) are implemented elsewhere). */ #include "precompiled.h" #include "ProfileViewer.h" #include "graphics/FontMetrics.h" #include "graphics/ShaderManager.h" #include "graphics/TextRenderer.h" #include "gui/GUIMatrix.h" #include "lib/external_libraries/libsdl.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Hotkey.h" #include "ps/Profile.h" +#include "ps/Pyrogenesis.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" #include #include extern int g_xres, g_yres; struct CProfileViewerInternals { NONCOPYABLE(CProfileViewerInternals); // because of the ofstream public: CProfileViewerInternals() {} /// Whether the profiling display is currently visible bool profileVisible; /// List of root tables std::vector rootTables; /// Path from a root table (path[0]) to the currently visible table (path[size-1]) std::vector path; /// Helper functions void TableIsDeleted(AbstractProfileTable* table); void NavigateTree(int id); /// File for saved profile output (reset when the game is restarted) std::ofstream outputStream; }; /////////////////////////////////////////////////////////////////////////////////////////////// // AbstractProfileTable implementation AbstractProfileTable::~AbstractProfileTable() { if (CProfileViewer::IsInitialised()) { g_ProfileViewer.m->TableIsDeleted(this); } } /////////////////////////////////////////////////////////////////////////////////////////////// // CProfileViewer implementation // AbstractProfileTable got deleted, make sure we have no dangling pointers void CProfileViewerInternals::TableIsDeleted(AbstractProfileTable* table) { for(int idx = (int)rootTables.size()-1; idx >= 0; --idx) { if (rootTables[idx] == table) rootTables.erase(rootTables.begin() + idx); } for(size_t idx = 0; idx < path.size(); ++idx) { if (path[idx] != table) continue; path.erase(path.begin() + idx, path.end()); if (path.size() == 0) profileVisible = false; } } // Move into child tables or return to parent tables based on the given number void CProfileViewerInternals::NavigateTree(int id) { if (id == 0) { if (path.size() > 1) path.pop_back(); } else { AbstractProfileTable* table = path[path.size() - 1]; size_t numrows = table->GetNumberRows(); for(size_t row = 0; row < numrows; ++row) { AbstractProfileTable* child = table->GetChild(row); if (!child) continue; --id; if (id == 0) { path.push_back(child); break; } } } } // Construction/Destruction CProfileViewer::CProfileViewer() { m = new CProfileViewerInternals; m->profileVisible = false; } CProfileViewer::~CProfileViewer() { delete m; } // Render void CProfileViewer::RenderProfile() { if (!m->profileVisible) return; if (!m->path.size()) { m->profileVisible = false; return; } PROFILE3_GPU("profile viewer"); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); AbstractProfileTable* table = m->path[m->path.size() - 1]; const std::vector& columns = table->GetColumns(); size_t numrows = table->GetNumberRows(); CStrIntern font_name("mono-stroke-10"); CFontMetrics font(font_name); int lineSpacing = font.GetLineSpacing(); // Render background GLint estimate_height; GLint estimate_width; estimate_width = 50; for(size_t i = 0; i < columns.size(); ++i) estimate_width += (GLint)columns[i].width; estimate_height = 3 + (GLint)numrows; if (m->path.size() > 1) estimate_height += 2; estimate_height = lineSpacing*estimate_height; CShaderTechniquePtr solidTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid); solidTech->BeginPass(); CShaderProgramPtr solidShader = solidTech->GetShader(); solidShader->Uniform(str_color, 0.0f, 0.0f, 0.0f, 0.5f); CMatrix3D transform = GetDefaultGuiMatrix(); solidShader->Uniform(str_transform, transform); float backgroundVerts[] = { (float)estimate_width, 0.0f, 0.0f, 0.0f, 0.0f, (float)estimate_height, 0.0f, (float)estimate_height, (float)estimate_width, (float)estimate_height, (float)estimate_width, 0.0f }; solidShader->VertexPointer(2, GL_FLOAT, 0, backgroundVerts); solidShader->AssertPointersBound(); glDrawArrays(GL_TRIANGLES, 0, 6); transform.PostTranslate(22.0f, lineSpacing*3.0f, 0.0f); solidShader->Uniform(str_transform, transform); // Draw row backgrounds for (size_t row = 0; row < numrows; ++row) { if (row % 2) solidShader->Uniform(str_color, 1.0f, 1.0f, 1.0f, 0.1f); else solidShader->Uniform(str_color, 0.0f, 0.0f, 0.0f, 0.1f); float rowVerts[] = { -22.f, 2.f, estimate_width-22.f, 2.f, estimate_width-22.f, 2.f-lineSpacing, estimate_width-22.f, 2.f-lineSpacing, -22.f, 2.f-lineSpacing, -22.f, 2.f }; solidShader->VertexPointer(2, GL_FLOAT, 0, rowVerts); solidShader->AssertPointersBound(); glDrawArrays(GL_TRIANGLES, 0, 6); transform.PostTranslate(0.0f, lineSpacing, 0.0f); solidShader->Uniform(str_transform, transform); } solidTech->EndPass(); // Print table and column titles CShaderTechniquePtr textTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text); textTech->BeginPass(); CTextRenderer textRenderer(textTech->GetShader()); textRenderer.Font(font_name); textRenderer.Color(1.0f, 1.0f, 1.0f); textRenderer.PrintfAt(2.0f, lineSpacing, L"%hs", table->GetTitle().c_str()); textRenderer.Translate(22.0f, lineSpacing*2.0f, 0.0f); float colX = 0.0f; for (size_t col = 0; col < columns.size(); ++col) { CStrW text = columns[col].title.FromUTF8(); int w, h; font.CalculateStringSize(text.c_str(), w, h); float x = colX; if (col > 0) // right-align all but the first column x += columns[col].width - w; textRenderer.Put(x, 0.0f, text.c_str()); colX += columns[col].width; } textRenderer.Translate(0.0f, lineSpacing, 0.0f); // Print rows int currentExpandId = 1; for (size_t row = 0; row < numrows; ++row) { if (table->IsHighlightRow(row)) textRenderer.Color(1.0f, 0.5f, 0.5f); else textRenderer.Color(1.0f, 1.0f, 1.0f); if (table->GetChild(row)) { textRenderer.PrintfAt(-15.0f, 0.0f, L"%d", currentExpandId); currentExpandId++; } float colX = 0.0f; for (size_t col = 0; col < columns.size(); ++col) { CStrW text = table->GetCellText(row, col).FromUTF8(); int w, h; font.CalculateStringSize(text.c_str(), w, h); float x = colX; if (col > 0) // right-align all but the first column x += columns[col].width - w; textRenderer.Put(x, 0.0f, text.c_str()); colX += columns[col].width; } textRenderer.Translate(0.0f, lineSpacing, 0.0f); } textRenderer.Color(1.0f, 1.0f, 1.0f); if (m->path.size() > 1) { textRenderer.Translate(0.0f, lineSpacing, 0.0f); textRenderer.Put(-15.0f, 0.0f, L"0"); textRenderer.Put(0.0f, 0.0f, L"back to parent"); } textRenderer.Render(); textTech->EndPass(); glDisable(GL_BLEND); glEnable(GL_DEPTH_TEST); } // Handle input InReaction CProfileViewer::Input(const SDL_Event_* ev) { switch(ev->ev.type) { case SDL_KEYDOWN: { if (!m->profileVisible) break; int k = ev->ev.key.keysym.sym; if (k >= SDLK_0 && k <= SDLK_9) { m->NavigateTree(k - SDLK_0); return IN_HANDLED; } break; } case SDL_HOTKEYDOWN: std::string hotkey = static_cast(ev->ev.user.data1); if( hotkey == "profile.toggle" ) { if (!m->profileVisible) { if (m->rootTables.size()) { m->profileVisible = true; m->path.push_back(m->rootTables[0]); } } else { size_t i; for(i = 0; i < m->rootTables.size(); ++i) { if (m->rootTables[i] == m->path[0]) break; } i++; m->path.clear(); if (i < m->rootTables.size()) { m->path.push_back(m->rootTables[i]); } else { m->profileVisible = false; } } return( IN_HANDLED ); } else if( hotkey == "profile.save" ) { SaveToFile(); return( IN_HANDLED ); } break; } return( IN_PASS ); } InReaction CProfileViewer::InputThunk(const SDL_Event_* ev) { if (CProfileViewer::IsInitialised()) return g_ProfileViewer.Input(ev); return IN_PASS; } // Add a table to the list of roots void CProfileViewer::AddRootTable(AbstractProfileTable* table, bool front) { if (front) m->rootTables.insert(m->rootTables.begin(), table); else m->rootTables.push_back(table); } namespace { struct WriteTable { std::ofstream& f; WriteTable(std::ofstream& f) : f(f) {} void operator() (AbstractProfileTable* table) { std::vector data; // 2d array of (rows+head)*columns elements const std::vector& columns = table->GetColumns(); // Add column headers to 'data' for (std::vector::const_iterator col_it = columns.begin(); col_it != columns.end(); ++col_it) data.push_back(col_it->title); // Recursively add all profile data to 'data' WriteRows(1, table, data); // Calculate the width of each column ( = the maximum width of // any value in that column) std::vector columnWidths; size_t cols = columns.size(); for (size_t c = 0; c < cols; ++c) { size_t max = 0; for (size_t i = c; i < data.size(); i += cols) max = std::max(max, data[i].length()); columnWidths.push_back(max); } // Output data as a formatted table: f << "\n\n" << table->GetTitle() << "\n"; if (cols == 0) // avoid divide-by-zero return; for (size_t r = 0; r < data.size()/cols; ++r) { for (size_t c = 0; c < cols; ++c) f << (c ? " | " : "\n") << data[r*cols + c].Pad(PS_TRIM_RIGHT, columnWidths[c]); // Add dividers under some rows. (Currently only the first, since // that contains the column headers.) if (r == 0) for (size_t c = 0; c < cols; ++c) f << (c ? "-|-" : "\n") << CStr::Repeat("-", columnWidths[c]); } } void WriteRows(int indent, AbstractProfileTable* table, std::vector& data) { const std::vector& columns = table->GetColumns(); for (size_t r = 0; r < table->GetNumberRows(); ++r) { // Do pretty tree-structure indenting CStr indentation = CStr::Repeat("| ", indent-1); if (r+1 == table->GetNumberRows()) indentation += "'-"; else indentation += "|-"; for (size_t c = 0; c < columns.size(); ++c) if (c == 0) data.push_back(indentation + table->GetCellText(r, c)); else data.push_back(table->GetCellText(r, c)); if (table->GetChild(r)) WriteRows(indent+1, table->GetChild(r), data); } } private: const WriteTable& operator=(const WriteTable&); }; struct DumpTable { const ScriptInterface& m_ScriptInterface; JS::PersistentRooted m_Root; DumpTable(const ScriptInterface& scriptInterface, JS::HandleValue root) : m_ScriptInterface(scriptInterface), m_Root(scriptInterface.GetJSRuntime(), root) { } // std::for_each requires a move constructor and the use of JS::PersistentRooted apparently breaks a requirement for an // automatic move constructor DumpTable(DumpTable && original) : m_ScriptInterface(original.m_ScriptInterface), m_Root(original.m_ScriptInterface.GetJSRuntime(), original.m_Root.get()) { } void operator() (AbstractProfileTable* table) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue t(cx); ScriptInterface::CreateObject( cx, &t, "cols", DumpCols(table), "data", DumpRows(table)); m_ScriptInterface.SetProperty(m_Root, table->GetTitle().c_str(), t); } std::vector DumpCols(AbstractProfileTable* table) { std::vector titles; const std::vector& columns = table->GetColumns(); for (size_t c = 0; c < columns.size(); ++c) titles.push_back(columns[c].title); return titles; } JS::Value DumpRows(AbstractProfileTable* table) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue data(cx); ScriptInterface::CreateObject(cx, &data); const std::vector& columns = table->GetColumns(); for (size_t r = 0; r < table->GetNumberRows(); ++r) { JS::RootedValue row(cx); ScriptInterface::CreateArray(cx, &row); m_ScriptInterface.SetProperty(data, table->GetCellText(r, 0).c_str(), row); if (table->GetChild(r)) { JS::RootedValue childRows(cx, DumpRows(table->GetChild(r))); m_ScriptInterface.SetPropertyInt(row, 0, childRows); } for (size_t c = 1; c < columns.size(); ++c) m_ScriptInterface.SetPropertyInt(row, c, table->GetCellText(r, c)); } return data; } private: const DumpTable& operator=(const DumpTable&); }; bool SortByName(AbstractProfileTable* a, AbstractProfileTable* b) { return (a->GetName() < b->GetName()); } } void CProfileViewer::SaveToFile() { // Open the file, if necessary. If this method is called several times, // the profile results will be appended to the previous ones from the same // run. if (! m->outputStream.is_open()) { // Open the file. (It will be closed when the CProfileViewer // destructor is called.) OsPath path = psLogDir()/"profile.txt"; m->outputStream.open(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); if (m->outputStream.fail()) { LOGERROR("Failed to open profile log file"); return; } else { LOGMESSAGERENDER("Profiler snapshot saved to '%s'", path.string8()); } } time_t t; time(&t); m->outputStream << "================================================================\n\n"; m->outputStream << "PS profiler snapshot - " << asctime(localtime(&t)); std::vector tables = m->rootTables; sort(tables.begin(), tables.end(), SortByName); for_each(tables.begin(), tables.end(), WriteTable(m->outputStream)); m->outputStream << "\n\n================================================================\n"; m->outputStream.flush(); } void CProfileViewer::ShowTable(const CStr& table) { m->path.clear(); if (table.length() > 0) { for (size_t i = 0; i < m->rootTables.size(); ++i) { if (m->rootTables[i]->GetName() == table) { m->path.push_back(m->rootTables[i]); m->profileVisible = true; return; } } } // No matching table found, so don't display anything m->profileVisible = false; } Index: ps/trunk/source/ps/Profiler2.cpp =================================================================== --- ps/trunk/source/ps/Profiler2.cpp (revision 23320) +++ ps/trunk/source/ps/Profiler2.cpp (revision 23321) @@ -1,981 +1,983 @@ /* Copyright (C) 2019 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "precompiled.h" #include "Profiler2.h" #include "lib/allocators/shared_ptr.h" +#include "lib/os_path.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Profiler2GPU.h" +#include "ps/Pyrogenesis.h" #include "third_party/mongoose/mongoose.h" #include #include #include CProfiler2 g_Profiler2; const size_t CProfiler2::MAX_ATTRIBUTE_LENGTH = 256; // TODO: what's a good size? const size_t CProfiler2::BUFFER_SIZE = 4 * 1024 * 1024; const size_t CProfiler2::HOLD_BUFFER_SIZE = 128 * 1024; // A human-recognisable pattern (for debugging) followed by random bytes (for uniqueness) const u8 CProfiler2::RESYNC_MAGIC[8] = {0x11, 0x22, 0x33, 0x44, 0xf4, 0x93, 0xbe, 0x15}; thread_local CProfiler2::ThreadStorage* CProfiler2::m_CurrentStorage = nullptr; CProfiler2::CProfiler2() : m_Initialised(false), m_FrameNumber(0), m_MgContext(NULL), m_GPU(NULL) { } CProfiler2::~CProfiler2() { if (m_Initialised) Shutdown(); } /** * Mongoose callback. Run in an arbitrary thread (possibly concurrently with other requests). */ static void* MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info) { CProfiler2* profiler = (CProfiler2*)request_info->user_data; ENSURE(profiler); void* handled = (void*)""; // arbitrary non-NULL pointer to indicate successful handling const char* header200 = "HTTP/1.1 200 OK\r\n" "Access-Control-Allow-Origin: *\r\n" // TODO: not great for security "Content-Type: text/plain; charset=utf-8\r\n\r\n"; const char* header404 = "HTTP/1.1 404 Not Found\r\n" "Content-Type: text/plain; charset=utf-8\r\n\r\n" "Unrecognised URI"; const char* header400 = "HTTP/1.1 400 Bad Request\r\n" "Content-Type: text/plain; charset=utf-8\r\n\r\n" "Invalid request"; switch (event) { case MG_NEW_REQUEST: { std::stringstream stream; std::string uri = request_info->uri; if (uri == "/download") { profiler->SaveToFile(); } else if (uri == "/overview") { profiler->ConstructJSONOverview(stream); } else if (uri == "/query") { if (!request_info->query_string) { mg_printf(conn, "%s (no query string)", header400); return handled; } // Identify the requested thread char buf[256]; int len = mg_get_var(request_info->query_string, strlen(request_info->query_string), "thread", buf, ARRAY_SIZE(buf)); if (len < 0) { mg_printf(conn, "%s (no 'thread')", header400); return handled; } std::string thread(buf); const char* err = profiler->ConstructJSONResponse(stream, thread); if (err) { mg_printf(conn, "%s (%s)", header400, err); return handled; } } else { mg_printf(conn, "%s", header404); return handled; } mg_printf(conn, "%s", header200); std::string str = stream.str(); mg_write(conn, str.c_str(), str.length()); return handled; } case MG_HTTP_ERROR: return NULL; case MG_EVENT_LOG: // Called by Mongoose's cry() LOGERROR("Mongoose error: %s", request_info->log_message); return NULL; case MG_INIT_SSL: return NULL; default: debug_warn(L"Invalid Mongoose event type"); return NULL; } }; void CProfiler2::Initialise() { ENSURE(!m_Initialised); m_Initialised = true; RegisterCurrentThread("main"); } void CProfiler2::InitialiseGPU() { ENSURE(!m_GPU); m_GPU = new CProfiler2GPU(*this); } void CProfiler2::EnableHTTP() { ENSURE(m_Initialised); LOGMESSAGERENDER("Starting profiler2 HTTP server"); // Ignore multiple enablings if (m_MgContext) return; const char *options[] = { "listening_ports", "127.0.0.1:8000", // bind to localhost for security "num_threads", "6", // enough for the browser's parallel connection limit NULL }; m_MgContext = mg_start(MgCallback, this, options); ENSURE(m_MgContext); } void CProfiler2::EnableGPU() { ENSURE(m_Initialised); if (!m_GPU) { LOGMESSAGERENDER("Starting profiler2 GPU mode"); InitialiseGPU(); } } void CProfiler2::ShutdownGPU() { LOGMESSAGERENDER("Shutting down profiler2 GPU mode"); SAFE_DELETE(m_GPU); } void CProfiler2::ShutDownHTTP() { LOGMESSAGERENDER("Shutting down profiler2 HTTP server"); if (m_MgContext) { mg_stop(m_MgContext); m_MgContext = NULL; } } void CProfiler2::Toggle() { // TODO: Maybe we can open the browser to the profiler page automatically if (m_GPU && m_MgContext) { ShutdownGPU(); ShutDownHTTP(); } else if (!m_GPU && !m_MgContext) { EnableGPU(); EnableHTTP(); } } void CProfiler2::Shutdown() { ENSURE(m_Initialised); ENSURE(!m_GPU); // must shutdown GPU before profiler if (m_MgContext) { mg_stop(m_MgContext); m_MgContext = NULL; } // the destructor is not called for the main thread // we have to call it manually to avoid memory leaks ENSURE(ThreadUtil::IsMainThread()); m_Initialised = false; } void CProfiler2::RecordGPUFrameStart() { if (m_GPU) m_GPU->FrameStart(); } void CProfiler2::RecordGPUFrameEnd() { if (m_GPU) m_GPU->FrameEnd(); } void CProfiler2::RecordGPURegionEnter(const char* id) { if (m_GPU) m_GPU->RegionEnter(id); } void CProfiler2::RecordGPURegionLeave(const char* id) { if (m_GPU) m_GPU->RegionLeave(id); } void CProfiler2::RegisterCurrentThread(const std::string& name) { ENSURE(m_Initialised); // Must not register a thread more than once. ENSURE(m_CurrentStorage == nullptr); m_CurrentStorage = new ThreadStorage(*this, name); AddThreadStorage(m_CurrentStorage); RecordSyncMarker(); RecordEvent("thread start"); } void CProfiler2::AddThreadStorage(ThreadStorage* storage) { std::lock_guard lock(m_Mutex); m_Threads.push_back(std::unique_ptr(storage)); } void CProfiler2::RemoveThreadStorage(ThreadStorage* storage) { std::lock_guard lock(m_Mutex); m_Threads.erase(std::find_if(m_Threads.begin(), m_Threads.end(), [storage](const std::unique_ptr& s) { return s.get() == storage; })); } CProfiler2::ThreadStorage::ThreadStorage(CProfiler2& profiler, const std::string& name) : m_Profiler(profiler), m_Name(name), m_BufferPos0(0), m_BufferPos1(0), m_LastTime(timer_Time()), m_HeldDepth(0) { m_Buffer = new u8[BUFFER_SIZE]; memset(m_Buffer, ITEM_NOP, BUFFER_SIZE); } CProfiler2::ThreadStorage::~ThreadStorage() { delete[] m_Buffer; } void CProfiler2::ThreadStorage::Write(EItem type, const void* item, u32 itemSize) { if (m_HeldDepth > 0) { WriteHold(type, item, itemSize); return; } // See m_BufferPos0 etc for comments on synchronisation u32 size = 1 + itemSize; u32 start = m_BufferPos0; if (start + size > BUFFER_SIZE) { // The remainder of the buffer is too small - fill the rest // with NOPs then start from offset 0, so we don't have to // bother splitting the real item across the end of the buffer m_BufferPos0 = size; COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer memset(m_Buffer + start, 0, BUFFER_SIZE - start); start = 0; } else { m_BufferPos0 = start + size; COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer } m_Buffer[start] = (u8)type; memcpy(&m_Buffer[start + 1], item, itemSize); COMPILER_FENCE; // must write m_BufferPos1 after m_Buffer m_BufferPos1 = start + size; } void CProfiler2::ThreadStorage::WriteHold(EItem type, const void* item, u32 itemSize) { u32 size = 1 + itemSize; if (m_HoldBuffers[m_HeldDepth - 1].pos + size > CProfiler2::HOLD_BUFFER_SIZE) return; // we held on too much data, ignore the rest m_HoldBuffers[m_HeldDepth - 1].buffer[m_HoldBuffers[m_HeldDepth - 1].pos] = (u8)type; memcpy(&m_HoldBuffers[m_HeldDepth - 1].buffer[m_HoldBuffers[m_HeldDepth - 1].pos + 1], item, itemSize); m_HoldBuffers[m_HeldDepth - 1].pos += size; } std::string CProfiler2::ThreadStorage::GetBuffer() { // Called from an arbitrary thread (not the one writing to the buffer). // // See comments on m_BufferPos0 etc. shared_ptr buffer(new u8[BUFFER_SIZE], ArrayDeleter()); u32 pos1 = m_BufferPos1; COMPILER_FENCE; // must read m_BufferPos1 before m_Buffer memcpy(buffer.get(), m_Buffer, BUFFER_SIZE); COMPILER_FENCE; // must read m_BufferPos0 after m_Buffer u32 pos0 = m_BufferPos0; // The range [pos1, pos0) modulo BUFFER_SIZE is invalid, so concatenate the rest of the buffer if (pos1 <= pos0) // invalid range is in the middle of the buffer return std::string(buffer.get()+pos0, buffer.get()+BUFFER_SIZE) + std::string(buffer.get(), buffer.get()+pos1); else // invalid wrap is wrapped around the end/start buffer return std::string(buffer.get()+pos0, buffer.get()+pos1); } void CProfiler2::ThreadStorage::RecordAttribute(const char* fmt, va_list argp) { char buffer[MAX_ATTRIBUTE_LENGTH + 4] = {0}; // first 4 bytes are used for storing length int len = vsnprintf(buffer + 4, MAX_ATTRIBUTE_LENGTH - 1, fmt, argp); // subtract 1 from length to make MSVC vsnprintf safe // (Don't use vsprintf_s because it treats overflow as fatal) // Terminate the string if the printing was truncated if (len < 0 || len >= (int)MAX_ATTRIBUTE_LENGTH - 1) { strncpy(buffer + 4 + MAX_ATTRIBUTE_LENGTH - 4, "...", 4); len = MAX_ATTRIBUTE_LENGTH - 1; // excluding null terminator } // Store the length in the buffer memcpy(buffer, &len, sizeof(len)); Write(ITEM_ATTRIBUTE, buffer, 4 + len); } size_t CProfiler2::ThreadStorage::HoldLevel() { return m_HeldDepth; } u8 CProfiler2::ThreadStorage::HoldType() { return m_HoldBuffers[m_HeldDepth - 1].type; } void CProfiler2::ThreadStorage::PutOnHold(u8 newType) { m_HeldDepth++; m_HoldBuffers[m_HeldDepth - 1].clear(); m_HoldBuffers[m_HeldDepth - 1].setType(newType); } // this flattens the stack, use it sensibly void rewriteBuffer(u8* buffer, u32& bufferSize) { double startTime = timer_Time(); u32 size = bufferSize; u32 readPos = 0; double initialTime = -1; double total_time = -1; const char* regionName; std::set topLevelArgs; using infoPerType = std::tuple >; using timeByTypeMap = std::unordered_map; timeByTypeMap timeByType; std::vector last_time_stack; std::vector last_names; // never too many hacks std::string current_attribute = ""; std::map time_per_attribute; // Let's read the first event { u8 type = buffer[readPos]; ++readPos; if (type != CProfiler2::ITEM_ENTER) { debug_warn("Profiler2: Condensing a region should run into ITEM_ENTER first"); return; // do nothing } CProfiler2::SItem_dt_id item; memcpy(&item, buffer + readPos, sizeof(item)); readPos += sizeof(item); regionName = item.id; last_names.push_back(item.id); initialTime = (double)item.dt; } int enter = 1; int leaves = 0; // Read subsequent events. Flatten hierarchy because it would get too complicated otherwise. // To make sure time doesn't bloat, subtract time from nested events while (readPos < size) { u8 type = buffer[readPos]; ++readPos; switch (type) { case CProfiler2::ITEM_NOP: { // ignore break; } case CProfiler2::ITEM_SYNC: { debug_warn("Aggregated regions should not be used across frames"); // still try to act sane readPos += sizeof(double); readPos += sizeof(CProfiler2::RESYNC_MAGIC); break; } case CProfiler2::ITEM_EVENT: { // skip for now readPos += sizeof(CProfiler2::SItem_dt_id); break; } case CProfiler2::ITEM_ENTER: { enter++; CProfiler2::SItem_dt_id item; memcpy(&item, buffer + readPos, sizeof(item)); readPos += sizeof(item); last_time_stack.push_back((double)item.dt); last_names.push_back(item.id); current_attribute = ""; break; } case CProfiler2::ITEM_LEAVE: { float item_time; memcpy(&item_time, buffer + readPos, sizeof(float)); readPos += sizeof(float); leaves++; if (last_names.empty()) { // we somehow lost the first entry in the process debug_warn("Invalid buffer for condensing"); } const char* item_name = last_names.back(); last_names.pop_back(); if (last_time_stack.empty()) { // this is the leave for the whole scope total_time = (double)item_time; break; } double time = (double)item_time - last_time_stack.back(); std::string name = std::string(item_name); timeByTypeMap::iterator TimeForType = timeByType.find(name); if (TimeForType == timeByType.end()) { // keep reference to the original char pointer to make sure we don't break things down the line std::get<0>(timeByType[name]) = item_name; std::get<1>(timeByType[name]) = 0; } std::get<1>(timeByType[name]) += time; last_time_stack.pop_back(); // if we were nested, subtract our time from the below scope by making it look like it starts later if (!last_time_stack.empty()) last_time_stack.back() += time; if (!current_attribute.empty()) { time_per_attribute[current_attribute] += time; } break; } case CProfiler2::ITEM_ATTRIBUTE: { // skip for now u32 len; memcpy(&len, buffer + readPos, sizeof(len)); ENSURE(len <= CProfiler2::MAX_ATTRIBUTE_LENGTH); readPos += sizeof(len); char message[CProfiler2::MAX_ATTRIBUTE_LENGTH] = {0}; memcpy(&message[0], buffer + readPos, std::min(size_t(len), CProfiler2::MAX_ATTRIBUTE_LENGTH)); CStr mess = CStr((const char*)message, len); if (!last_names.empty()) { timeByTypeMap::iterator it = timeByType.find(std::string(last_names.back())); if (it == timeByType.end()) topLevelArgs.insert(mess); else std::get<2>(timeByType[std::string(last_names.back())]).insert(mess); } readPos += len; current_attribute = mess; break; } default: debug_warn(L"Invalid profiler item when condensing buffer"); continue; } } // rewrite the buffer // what we rewrite will always be smaller than the current buffer's size u32 writePos = 0; double curTime = initialTime; // the region enter { CProfiler2::SItem_dt_id item = { (float)curTime, regionName }; buffer[writePos] = (u8)CProfiler2::ITEM_ENTER; memcpy(buffer + writePos + 1, &item, sizeof(item)); writePos += sizeof(item) + 1; // add a nanosecond for sanity curTime += 0.000001; } // sub-events, aggregated for (const std::pair& type : timeByType) { CProfiler2::SItem_dt_id item = { (float)curTime, std::get<0>(type.second) }; buffer[writePos] = (u8)CProfiler2::ITEM_ENTER; memcpy(buffer + writePos + 1, &item, sizeof(item)); writePos += sizeof(item) + 1; // write relevant attributes if present for (const std::string& attrib : std::get<2>(type.second)) { buffer[writePos] = (u8)CProfiler2::ITEM_ATTRIBUTE; writePos++; std::string basic = attrib; std::map::iterator time_attrib = time_per_attribute.find(attrib); if (time_attrib != time_per_attribute.end()) basic += " " + CStr::FromInt(1000000*time_attrib->second) + "us"; u32 length = basic.size(); memcpy(buffer + writePos, &length, sizeof(length)); writePos += sizeof(length); memcpy(buffer + writePos, basic.c_str(), length); writePos += length; } curTime += std::get<1>(type.second); float leave_time = (float)curTime; buffer[writePos] = (u8)CProfiler2::ITEM_LEAVE; memcpy(buffer + writePos + 1, &leave_time, sizeof(float)); writePos += sizeof(float) + 1; } // Time of computation { CProfiler2::SItem_dt_id item = { (float)curTime, "CondenseBuffer" }; buffer[writePos] = (u8)CProfiler2::ITEM_ENTER; memcpy(buffer + writePos + 1, &item, sizeof(item)); writePos += sizeof(item) + 1; } { float time_out = (float)(curTime + timer_Time() - startTime); buffer[writePos] = (u8)CProfiler2::ITEM_LEAVE; memcpy(buffer + writePos + 1, &time_out, sizeof(float)); writePos += sizeof(float) + 1; // add a nanosecond for sanity curTime += 0.000001; } // the region leave { if (total_time < 0) { total_time = curTime + 0.000001; buffer[writePos] = (u8)CProfiler2::ITEM_ATTRIBUTE; writePos++; u32 length = sizeof("buffer overflow"); memcpy(buffer + writePos, &length, sizeof(length)); writePos += sizeof(length); memcpy(buffer + writePos, "buffer overflow", length); writePos += length; } else if (total_time < curTime) { // this seems to happen on rare occasions. curTime = total_time; } float leave_time = (float)total_time; buffer[writePos] = (u8)CProfiler2::ITEM_LEAVE; memcpy(buffer + writePos + 1, &leave_time, sizeof(float)); writePos += sizeof(float) + 1; } bufferSize = writePos; } void CProfiler2::ThreadStorage::HoldToBuffer(bool condensed) { ENSURE(m_HeldDepth); if (condensed) { // rewrite the buffer to show aggregated data rewriteBuffer(m_HoldBuffers[m_HeldDepth - 1].buffer, m_HoldBuffers[m_HeldDepth - 1].pos); } if (m_HeldDepth > 1) { // copy onto buffer below HoldBuffer& copied = m_HoldBuffers[m_HeldDepth - 1]; HoldBuffer& target = m_HoldBuffers[m_HeldDepth - 2]; if (target.pos + copied.pos > HOLD_BUFFER_SIZE) return; // too much data, too bad memcpy(&target.buffer[target.pos], copied.buffer, copied.pos); target.pos += copied.pos; } else { u32 size = m_HoldBuffers[m_HeldDepth - 1].pos; u32 start = m_BufferPos0; if (start + size > BUFFER_SIZE) { m_BufferPos0 = size; COMPILER_FENCE; memset(m_Buffer + start, 0, BUFFER_SIZE - start); start = 0; } else { m_BufferPos0 = start + size; COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer } memcpy(&m_Buffer[start], m_HoldBuffers[m_HeldDepth - 1].buffer, size); COMPILER_FENCE; // must write m_BufferPos1 after m_Buffer m_BufferPos1 = start + size; } m_HeldDepth--; } void CProfiler2::ThreadStorage::ThrowawayHoldBuffer() { if (!m_HeldDepth) return; m_HeldDepth--; } void CProfiler2::ConstructJSONOverview(std::ostream& stream) { TIMER(L"profile2 overview"); std::lock_guard lock(m_Mutex); stream << "{\"threads\":["; bool first_time = true; for (std::unique_ptr& storage : m_Threads) { if (!first_time) stream << ","; stream << "{\"name\":\"" << CStr(storage->GetName()).EscapeToPrintableASCII() << "\"}"; first_time = false; } stream << "]}"; } /** * Given a buffer and a visitor class (with functions OnEvent, OnEnter, OnLeave, OnAttribute), * calls the visitor for every item in the buffer. */ template void RunBufferVisitor(const std::string& buffer, V& visitor) { TIMER(L"profile2 visitor"); // The buffer doesn't necessarily start at the beginning of an item // (we just grabbed it from some arbitrary point in the middle), // so scan forwards until we find a sync marker. // (This is probably pretty inefficient.) u32 realStart = (u32)-1; // the start point decided by the scan algorithm for (u32 start = 0; start + 1 + sizeof(CProfiler2::RESYNC_MAGIC) <= buffer.length(); ++start) { if (buffer[start] == CProfiler2::ITEM_SYNC && memcmp(buffer.c_str() + start + 1, &CProfiler2::RESYNC_MAGIC, sizeof(CProfiler2::RESYNC_MAGIC)) == 0) { realStart = start; break; } } ENSURE(realStart != (u32)-1); // we should have found a sync point somewhere in the buffer u32 pos = realStart; // the position as we step through the buffer double lastTime = -1; // set to non-negative by EVENT_SYNC; we ignore all items before that // since we can't compute their absolute times while (pos < buffer.length()) { u8 type = buffer[pos]; ++pos; switch (type) { case CProfiler2::ITEM_NOP: { // ignore break; } case CProfiler2::ITEM_SYNC: { u8 magic[sizeof(CProfiler2::RESYNC_MAGIC)]; double t; memcpy(magic, buffer.c_str()+pos, ARRAY_SIZE(magic)); ENSURE(memcmp(magic, &CProfiler2::RESYNC_MAGIC, sizeof(CProfiler2::RESYNC_MAGIC)) == 0); pos += sizeof(CProfiler2::RESYNC_MAGIC); memcpy(&t, buffer.c_str()+pos, sizeof(t)); pos += sizeof(t); lastTime = t; visitor.OnSync(lastTime); break; } case CProfiler2::ITEM_EVENT: { CProfiler2::SItem_dt_id item; memcpy(&item, buffer.c_str()+pos, sizeof(item)); pos += sizeof(item); if (lastTime >= 0) { visitor.OnEvent(lastTime + (double)item.dt, item.id); } break; } case CProfiler2::ITEM_ENTER: { CProfiler2::SItem_dt_id item; memcpy(&item, buffer.c_str()+pos, sizeof(item)); pos += sizeof(item); if (lastTime >= 0) { visitor.OnEnter(lastTime + (double)item.dt, item.id); } break; } case CProfiler2::ITEM_LEAVE: { float leave_time; memcpy(&leave_time, buffer.c_str() + pos, sizeof(float)); pos += sizeof(float); if (lastTime >= 0) { visitor.OnLeave(lastTime + (double)leave_time); } break; } case CProfiler2::ITEM_ATTRIBUTE: { u32 len; memcpy(&len, buffer.c_str()+pos, sizeof(len)); ENSURE(len <= CProfiler2::MAX_ATTRIBUTE_LENGTH); pos += sizeof(len); std::string attribute(buffer.c_str()+pos, buffer.c_str()+pos+len); pos += len; if (lastTime >= 0) { visitor.OnAttribute(attribute); } break; } default: debug_warn(L"Invalid profiler item when parsing buffer"); return; } } }; /** * Visitor class that dumps events as JSON. * TODO: this is pretty inefficient (in implementation and in output format). */ struct BufferVisitor_Dump { NONCOPYABLE(BufferVisitor_Dump); public: BufferVisitor_Dump(std::ostream& stream) : m_Stream(stream) { } void OnSync(double UNUSED(time)) { // Split the array of items into an array of array (arbitrarily splitting // around the sync points) to avoid array-too-large errors in JSON decoders m_Stream << "null], [\n"; } void OnEvent(double time, const char* id) { m_Stream << "[1," << std::fixed << std::setprecision(9) << time; m_Stream << ",\"" << CStr(id).EscapeToPrintableASCII() << "\"],\n"; } void OnEnter(double time, const char* id) { m_Stream << "[2," << std::fixed << std::setprecision(9) << time; m_Stream << ",\"" << CStr(id).EscapeToPrintableASCII() << "\"],\n"; } void OnLeave(double time) { m_Stream << "[3," << std::fixed << std::setprecision(9) << time << "],\n"; } void OnAttribute(const std::string& attr) { m_Stream << "[4,\"" << CStr(attr).EscapeToPrintableASCII() << "\"],\n"; } std::ostream& m_Stream; }; const char* CProfiler2::ConstructJSONResponse(std::ostream& stream, const std::string& thread) { TIMER(L"profile2 query"); std::string buffer; { TIMER(L"profile2 get buffer"); std::lock_guard lock(m_Mutex); // lock against changes to m_Threads or deletions of ThreadStorage std::vector>::iterator it = std::find_if(m_Threads.begin(), m_Threads.end(), [&thread](std::unique_ptr& storage) { return storage->GetName() == thread; }); if (it == m_Threads.end()) return "cannot find named thread"; stream << "{\"events\":[\n"; stream << "[\n"; buffer = (*it)->GetBuffer(); } BufferVisitor_Dump visitor(stream); RunBufferVisitor(buffer, visitor); stream << "null]\n]}"; return NULL; } void CProfiler2::SaveToFile() { OsPath path = psLogDir()/"profile2.jsonp"; std::ofstream stream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); ENSURE(stream.good()); stream << "profileDataCB({\"threads\": [\n"; bool first_time = true; for (std::unique_ptr& storage : m_Threads) { if (!first_time) stream << ",\n"; stream << "{\"name\":\"" << CStr(storage->GetName()).EscapeToPrintableASCII() << "\",\n"; stream << "\"data\": "; ConstructJSONResponse(stream, storage->GetName()); stream << "\n}"; first_time = false; } stream << "\n]});\n"; } CProfile2SpikeRegion::CProfile2SpikeRegion(const char* name, double spikeLimit) : m_Name(name), m_Limit(spikeLimit), m_PushedHold(true) { if (g_Profiler2.HoldLevel() < 8 && g_Profiler2.HoldType() != CProfiler2::ThreadStorage::BUFFER_AGGREGATE) g_Profiler2.HoldMessages(CProfiler2::ThreadStorage::BUFFER_SPIKE); else m_PushedHold = false; COMPILER_FENCE; g_Profiler2.RecordRegionEnter(m_Name); m_StartTime = g_Profiler2.GetTime(); } CProfile2SpikeRegion::~CProfile2SpikeRegion() { double time = g_Profiler2.GetTime(); g_Profiler2.RecordRegionLeave(); bool shouldWrite = time - m_StartTime > m_Limit; if (m_PushedHold) g_Profiler2.StopHoldingMessages(shouldWrite); } CProfile2AggregatedRegion::CProfile2AggregatedRegion(const char* name, double spikeLimit) : m_Name(name), m_Limit(spikeLimit), m_PushedHold(true) { if (g_Profiler2.HoldLevel() < 8 && g_Profiler2.HoldType() != CProfiler2::ThreadStorage::BUFFER_AGGREGATE) g_Profiler2.HoldMessages(CProfiler2::ThreadStorage::BUFFER_AGGREGATE); else m_PushedHold = false; COMPILER_FENCE; g_Profiler2.RecordRegionEnter(m_Name); m_StartTime = g_Profiler2.GetTime(); } CProfile2AggregatedRegion::~CProfile2AggregatedRegion() { double time = g_Profiler2.GetTime(); g_Profiler2.RecordRegionLeave(); bool shouldWrite = time - m_StartTime > m_Limit; if (m_PushedHold) g_Profiler2.StopHoldingMessages(shouldWrite, true); } Index: ps/trunk/source/ps/Replay.h =================================================================== --- ps/trunk/source/ps/Replay.h (revision 23320) +++ ps/trunk/source/ps/Replay.h (revision 23321) @@ -1,118 +1,119 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_REPLAY #define INCLUDED_REPLAY +#include "lib/os_path.h" #include "ps/CStr.h" #include "scriptinterface/ScriptTypes.h" struct SimulationCommand; class CSimulation2; class ScriptInterface; /** * Replay log recorder interface. * Call its methods at appropriate times during the game. */ class IReplayLogger { public: IReplayLogger() { } virtual ~IReplayLogger() { } /** * Started the game with the given game attributes. */ virtual void StartGame(JS::MutableHandleValue attribs) = 0; /** * Run the given turn with the given collection of player commands. */ virtual void Turn(u32 n, u32 turnLength, std::vector& commands) = 0; /** * Optional hash of simulation state (for sync checking). */ virtual void Hash(const std::string& hash, bool quick) = 0; /** * Saves metadata.json containing part of the simulation state used for the summary screen. */ virtual void SaveMetadata(const CSimulation2& simulation) = 0; /** * Remember the directory containing the commands.txt file, so that we can save additional files to it. */ virtual OsPath GetDirectory() const = 0; }; /** * Implementation of IReplayLogger that simply throws away all data. */ class CDummyReplayLogger : public IReplayLogger { public: virtual void StartGame(JS::MutableHandleValue UNUSED(attribs)) { } virtual void Turn(u32 UNUSED(n), u32 UNUSED(turnLength), std::vector& UNUSED(commands)) { } virtual void Hash(const std::string& UNUSED(hash), bool UNUSED(quick)) { } virtual void SaveMetadata(const CSimulation2& UNUSED(simulation)) { }; virtual OsPath GetDirectory() const { return OsPath(); } }; /** * Implementation of IReplayLogger that saves data to a file in the logs directory. */ class CReplayLogger : public IReplayLogger { NONCOPYABLE(CReplayLogger); public: CReplayLogger(const ScriptInterface& scriptInterface); ~CReplayLogger(); virtual void StartGame(JS::MutableHandleValue attribs); virtual void Turn(u32 n, u32 turnLength, std::vector& commands); virtual void Hash(const std::string& hash, bool quick); virtual void SaveMetadata(const CSimulation2& simulation); virtual OsPath GetDirectory() const; private: const ScriptInterface& m_ScriptInterface; std::ostream* m_Stream; OsPath m_Directory; }; /** * Replay log replayer. Runs the log with no graphics and dumps some info to stdout. */ class CReplayPlayer { public: CReplayPlayer(); ~CReplayPlayer(); void Load(const OsPath& path); void Replay(const bool serializationtest, const int rejointestturn, const bool ooslog, const bool testHashFull, const bool testHashQuick); private: std::istream* m_Stream; CStr ModListToString(const std::vector>& list) const; void CheckReplayMods(const ScriptInterface& scriptInterface, JS::HandleValue attribs) const; void TestHash(const std::string& hashType, const std::string& replayHash, const bool testHashFull, const bool testHashQuick); }; #endif // INCLUDED_REPLAY Index: ps/trunk/source/ps/UserReport.cpp =================================================================== --- ps/trunk/source/ps/UserReport.cpp (revision 23320) +++ ps/trunk/source/ps/UserReport.cpp (revision 23321) @@ -1,636 +1,637 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "UserReport.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lib/external_libraries/curl.h" #include "lib/external_libraries/zlib.h" #include "lib/file/archive/stream.h" #include "lib/os_path.h" #include "lib/sysdep/sysdep.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Profiler2.h" +#include "ps/Pyrogenesis.h" #include #include #include #include #include #define DEBUG_UPLOADS 0 /* * The basic idea is that the game submits reports to us, which we send over * HTTP to a server for storage and analysis. * * We can't use libcurl's asynchronous 'multi' API, because DNS resolution can * be synchronous and slow (which would make the game pause). * So we use the 'easy' API in a background thread. * The main thread submits reports, toggles whether uploading is enabled, * and polls for the current status (typically to display in the GUI); * the worker thread does all of the uploading. * * It'd be nice to extend this in the future to handle things like crash reports. * The game should store the crashlogs (suitably anonymised) in a directory, and * we should detect those files and upload them when we're restarted and online. */ /** * Version number stored in config file when the user agrees to the reporting. * Reporting will be disabled if the config value is missing or is less than * this value. If we start reporting a lot more data, we should increase this * value and get the user to re-confirm. */ static const int REPORTER_VERSION = 1; /** * Time interval (seconds) at which the worker thread will check its reconnection * timers. (This should be relatively high so the thread doesn't waste much time * continually waking up.) */ static const double TIMER_CHECK_INTERVAL = 10.0; /** * Seconds we should wait before reconnecting to the server after a failure. */ static const double RECONNECT_INVERVAL = 60.0; CUserReporter g_UserReporter; struct CUserReport { time_t m_Time; std::string m_Type; int m_Version; std::string m_Data; }; class CUserReporterWorker { public: CUserReporterWorker(const std::string& userID, const std::string& url) : m_URL(url), m_UserID(userID), m_Enabled(false), m_Shutdown(false), m_Status("disabled"), m_PauseUntilTime(timer_Time()), m_LastUpdateTime(timer_Time()) { // Set up libcurl: m_Curl = curl_easy_init(); ENSURE(m_Curl); #if DEBUG_UPLOADS curl_easy_setopt(m_Curl, CURLOPT_VERBOSE, 1L); #endif // Capture error messages curl_easy_setopt(m_Curl, CURLOPT_ERRORBUFFER, m_ErrorBuffer); // Disable signal handlers (required for multithreaded applications) curl_easy_setopt(m_Curl, CURLOPT_NOSIGNAL, 1L); // To minimise security risks, don't support redirects curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L); // Prevent this thread from blocking the engine shutdown for 5 minutes in case the server is unavailable curl_easy_setopt(m_Curl, CURLOPT_CONNECTTIMEOUT, 10L); // Set IO callbacks curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, ReceiveCallback); curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, this); curl_easy_setopt(m_Curl, CURLOPT_READFUNCTION, SendCallback); curl_easy_setopt(m_Curl, CURLOPT_READDATA, this); // Set URL to POST to curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(m_Curl, CURLOPT_POST, 1L); // Set up HTTP headers m_Headers = NULL; // Set the UA string std::string ua = "User-Agent: 0ad "; ua += curl_version(); ua += " (http://play0ad.com/)"; m_Headers = curl_slist_append(m_Headers, ua.c_str()); // Override the default application/x-www-form-urlencoded type since we're not using that type m_Headers = curl_slist_append(m_Headers, "Content-Type: application/octet-stream"); // Disable the Accept header because it's a waste of a dozen bytes m_Headers = curl_slist_append(m_Headers, "Accept: "); curl_easy_setopt(m_Curl, CURLOPT_HTTPHEADER, m_Headers); m_WorkerThread = std::thread(RunThread, this); } ~CUserReporterWorker() { curl_slist_free_all(m_Headers); curl_easy_cleanup(m_Curl); } /** * Called by main thread, when the online reporting is enabled/disabled. */ void SetEnabled(bool enabled) { std::lock_guard lock(m_WorkerMutex); if (enabled != m_Enabled) { m_Enabled = enabled; // Wake up the worker thread m_WorkerCV.notify_all(); } } /** * Called by main thread to request shutdown. * Returns true if we've shut down successfully. * Returns false if shutdown is taking too long (we might be blocked on a * sync network operation) - you mustn't destroy this object, just leak it * and terminate. */ bool Shutdown() { { std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; } // Wake up the worker thread m_WorkerCV.notify_all(); // Wait for it to shut down cleanly // TODO: should have a timeout in case of network hangs m_WorkerThread.join(); return true; } /** * Called by main thread to determine the current status of the uploader. */ std::string GetStatus() { std::lock_guard lock(m_WorkerMutex); return m_Status; } /** * Called by main thread to add a new report to the queue. */ void Submit(const shared_ptr& report) { { std::lock_guard lock(m_WorkerMutex); m_ReportQueue.push_back(report); } // Wake up the worker thread m_WorkerCV.notify_all(); } /** * Called by the main thread every frame, so we can check * retransmission timers. */ void Update() { double now = timer_Time(); if (now > m_LastUpdateTime + TIMER_CHECK_INTERVAL) { // Wake up the worker thread m_WorkerCV.notify_all(); m_LastUpdateTime = now; } } private: static void RunThread(CUserReporterWorker* data) { debug_SetThreadName("CUserReportWorker"); g_Profiler2.RegisterCurrentThread("userreport"); data->Run(); } void Run() { // Set libcurl's proxy configuration // (This has to be done in the thread because it's potentially very slow) SetStatus("proxy"); std::wstring proxy; { PROFILE2("get proxy config"); if (sys_get_proxy_config(wstring_from_utf8(m_URL), proxy) == INFO::OK) curl_easy_setopt(m_Curl, CURLOPT_PROXY, utf8_from_wstring(proxy).c_str()); } SetStatus("waiting"); /* * We use a condition_variable to let the thread be woken up when it has * work to do. Various actions from the main thread can wake it: * * SetEnabled() * * Shutdown() * * Submit() * * Retransmission timeouts, once every several seconds * * If multiple actions have triggered wakeups, we might respond to * all of those actions after the first wakeup, which is okay (we'll do * nothing during the subsequent wakeups). We should never hang due to * processing fewer actions than wakeups. * * Retransmission timeouts are triggered via the main thread. */ // Wait until the main thread wakes us up while (true) { g_Profiler2.RecordRegionEnter("condition_variable wait"); std::unique_lock lock(m_WorkerMutex); m_WorkerCV.wait(lock); lock.unlock(); g_Profiler2.RecordRegionLeave(); // Handle shutdown requests as soon as possible if (GetShutdown()) return; // If we're not enabled, ignore this wakeup if (!GetEnabled()) continue; // If we're still pausing due to a failed connection, // go back to sleep again if (timer_Time() < m_PauseUntilTime) continue; // We're enabled, so process as many reports as possible while (ProcessReport()) { // Handle shutdowns while we were sending the report if (GetShutdown()) return; } } } bool GetEnabled() { std::lock_guard lock(m_WorkerMutex); return m_Enabled; } bool GetShutdown() { std::lock_guard lock(m_WorkerMutex); return m_Shutdown; } void SetStatus(const std::string& status) { std::lock_guard lock(m_WorkerMutex); m_Status = status; #if DEBUG_UPLOADS debug_printf(">>> CUserReporterWorker status: %s\n", status.c_str()); #endif } bool ProcessReport() { PROFILE2("process report"); shared_ptr report; { std::lock_guard lock(m_WorkerMutex); if (m_ReportQueue.empty()) return false; report = m_ReportQueue.front(); m_ReportQueue.pop_front(); } ConstructRequestData(*report); m_RequestDataOffset = 0; m_ResponseData.clear(); m_ErrorBuffer[0] = '\0'; curl_easy_setopt(m_Curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)m_RequestData.size()); SetStatus("connecting"); #if DEBUG_UPLOADS TIMER(L"CUserReporterWorker request"); #endif CURLcode err = curl_easy_perform(m_Curl); #if DEBUG_UPLOADS printf(">>>\n%s\n<<<\n", m_ResponseData.c_str()); #endif if (err == CURLE_OK) { long code = -1; curl_easy_getinfo(m_Curl, CURLINFO_RESPONSE_CODE, &code); SetStatus("completed:" + CStr::FromInt(code)); // Check for success code if (code == 200) return true; // If the server returns the 410 Gone status, interpret that as meaning // it no longer supports uploads (at least from this version of the game), // so shut down and stop talking to it (to avoid wasting bandwidth) if (code == 410) { std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; return false; } } else { std::string errorString(m_ErrorBuffer); if (errorString.empty()) errorString = curl_easy_strerror(err); SetStatus("failed:" + CStr::FromInt(err) + ":" + errorString); } // We got an unhandled return code or a connection failure; // push this report back onto the queue and try again after // a long interval { std::lock_guard lock(m_WorkerMutex); m_ReportQueue.push_front(report); } m_PauseUntilTime = timer_Time() + RECONNECT_INVERVAL; return false; } void ConstructRequestData(const CUserReport& report) { // Construct the POST request data in the application/x-www-form-urlencoded format std::string r; r += "user_id="; AppendEscaped(r, m_UserID); r += "&time=" + CStr::FromInt64(report.m_Time); r += "&type="; AppendEscaped(r, report.m_Type); r += "&version=" + CStr::FromInt(report.m_Version); r += "&data="; AppendEscaped(r, report.m_Data); // Compress the content with zlib to save bandwidth. // (Note that we send a request with unlabelled compressed data instead // of using Content-Encoding, because Content-Encoding is a mess and causes // problems with servers and breaks Content-Length and this is much easier.) std::string compressed; compressed.resize(compressBound(r.size())); uLongf destLen = compressed.size(); int ok = compress((Bytef*)compressed.c_str(), &destLen, (const Bytef*)r.c_str(), r.size()); ENSURE(ok == Z_OK); compressed.resize(destLen); m_RequestData.swap(compressed); } void AppendEscaped(std::string& buffer, const std::string& str) { char* escaped = curl_easy_escape(m_Curl, str.c_str(), str.size()); buffer += escaped; curl_free(escaped); } static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp) { CUserReporterWorker* self = static_cast(userp); if (self->GetShutdown()) return 0; // signals an error self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb); return size*nmemb; } static size_t SendCallback(char* bufptr, size_t size, size_t nmemb, void* userp) { CUserReporterWorker* self = static_cast(userp); if (self->GetShutdown()) return CURL_READFUNC_ABORT; // signals an error // We can return as much data as available, up to the buffer size size_t amount = std::min(self->m_RequestData.size() - self->m_RequestDataOffset, size*nmemb); // ...But restrict to sending a small amount at once, so that we remain // responsive to shutdown requests even if the network is pretty slow amount = std::min((size_t)1024, amount); if(amount != 0) // (avoids invalid operator[] call where index=size) { memcpy(bufptr, &self->m_RequestData[self->m_RequestDataOffset], amount); self->m_RequestDataOffset += amount; } self->SetStatus("sending:" + CStr::FromDouble((double)self->m_RequestDataOffset / self->m_RequestData.size())); return amount; } private: // Thread-related members: std::thread m_WorkerThread; std::mutex m_WorkerMutex; std::condition_variable m_WorkerCV; // Shared by main thread and worker thread: // These variables are all protected by m_WorkerMutex std::deque > m_ReportQueue; bool m_Enabled; bool m_Shutdown; std::string m_Status; // Initialised in constructor by main thread; otherwise used only by worker thread: std::string m_URL; std::string m_UserID; CURL* m_Curl; curl_slist* m_Headers; double m_PauseUntilTime; // Only used by worker thread: std::string m_ResponseData; std::string m_RequestData; size_t m_RequestDataOffset; char m_ErrorBuffer[CURL_ERROR_SIZE]; // Only used by main thread: double m_LastUpdateTime; }; CUserReporter::CUserReporter() : m_Worker(NULL) { } CUserReporter::~CUserReporter() { ENSURE(!m_Worker); // Deinitialize should have been called before shutdown } std::string CUserReporter::LoadUserID() { std::string userID; // Read the user ID from user.cfg (if there is one) CFG_GET_VAL("userreport.id", userID); // If we don't have a validly-formatted user ID, generate a new one if (userID.length() != 16) { u8 bytes[8] = {0}; sys_generate_random_bytes(bytes, ARRAY_SIZE(bytes)); // ignore failures - there's not much we can do about it userID = ""; for (size_t i = 0; i < ARRAY_SIZE(bytes); ++i) { char hex[3]; sprintf_s(hex, ARRAY_SIZE(hex), "%02x", (unsigned int)bytes[i]); userID += hex; } g_ConfigDB.SetValueString(CFG_USER, "userreport.id", userID); g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.id", userID); } return userID; } bool CUserReporter::IsReportingEnabled() { int version = -1; CFG_GET_VAL("userreport.enabledversion", version); return (version >= REPORTER_VERSION); } void CUserReporter::SetReportingEnabled(bool enabled) { CStr val = CStr::FromInt(enabled ? REPORTER_VERSION : 0); g_ConfigDB.SetValueString(CFG_USER, "userreport.enabledversion", val); g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.enabledversion", val); if (m_Worker) m_Worker->SetEnabled(enabled); } std::string CUserReporter::GetStatus() { if (!m_Worker) return "disabled"; return m_Worker->GetStatus(); } void CUserReporter::Initialize() { ENSURE(!m_Worker); // must only be called once std::string userID = LoadUserID(); std::string url; CFG_GET_VAL("userreport.url_upload", url); m_Worker = new CUserReporterWorker(userID, url); m_Worker->SetEnabled(IsReportingEnabled()); } void CUserReporter::Deinitialize() { if (!m_Worker) return; if (m_Worker->Shutdown()) { // Worker was shut down cleanly SAFE_DELETE(m_Worker); } else { // Worker failed to shut down in a reasonable time // Leak the resources (since that's better than hanging or crashing) m_Worker = NULL; } } void CUserReporter::Update() { if (m_Worker) m_Worker->Update(); } void CUserReporter::SubmitReport(const std::string& type, int version, const std::string& data, const std::string& dataHumanReadable) { // Write to logfile, enabling users to assess privacy concerns before the data is submitted if (!dataHumanReadable.empty()) { OsPath path = psLogDir() / OsPath("userreport_" + type + ".txt"); std::ofstream stream(OsString(path), std::ofstream::trunc); if (stream) { debug_printf("UserReport written to %s\n", path.string8().c_str()); stream << dataHumanReadable << std::endl; stream.close(); } else debug_printf("Failed to write UserReport to %s\n", path.string8().c_str()); } // If not initialised, discard the report if (!m_Worker) return; // Actual submit shared_ptr report(new CUserReport); report->m_Time = time(NULL); report->m_Type = type; report->m_Version = version; report->m_Data = data; m_Worker->Submit(report); } Index: ps/trunk/source/ps/Util.cpp =================================================================== --- ps/trunk/source/ps/Util.cpp (revision 23320) +++ ps/trunk/source/ps/Util.cpp (revision 23321) @@ -1,451 +1,452 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ps/Util.h" #include "lib/posix/posix_utsname.h" #include "lib/ogl.h" #include "lib/snd.h" #include "lib/timer.h" #include "lib/bits.h" // round_up #include "lib/allocators/shared_ptr.h" #include "lib/sysdep/sysdep.h" // sys_OpenFile #include "lib/sysdep/gfx.h" #include "lib/sysdep/cpu.h" #include "lib/sysdep/os_cpu.h" #if ARCH_X86_X64 #include "lib/sysdep/arch/x86_x64/topology.h" #endif #include "lib/sysdep/smbios.h" #include "lib/tex/tex.h" #include "i18n/L10n.h" #include "lib/utf8.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/GameSetup.h" #include "ps/Game.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" +#include "ps/Pyrogenesis.h" #include "ps/VideoMode.h" #include "renderer/Renderer.h" #include "maths/MathUtil.h" #include "graphics/GameView.h" #include #include extern CStrW g_CursorName; static std::string SplitExts(const char *exts) { std::string str = exts; std::string ret = ""; size_t idx = str.find_first_of(" "); while(idx != std::string::npos) { if(idx >= str.length() - 1) { ret += str; break; } ret += str.substr(0, idx); ret += "\n"; str = str.substr(idx + 1); idx = str.find_first_of(" "); } return ret; } void WriteSystemInfo() { TIMER(L"write_sys_info"); // get_cpu_info and gfx_detect already called during init - see call site snd_detect(); struct utsname un; uname(&un); OsPath pathname = psLogDir()/"system_info.txt"; FILE* f = sys_OpenFile(pathname, "w"); if(!f) return; // current timestamp (redundant WRT OS timestamp, but that is not // visible when people are posting this file's contents online) { wchar_t timestampBuf[100] = {'\0'}; time_t seconds; time(&seconds); struct tm* t = gmtime(&seconds); const size_t charsWritten = wcsftime(timestampBuf, ARRAY_SIZE(timestampBuf), L"(generated %Y-%m-%d %H:%M:%S UTC)", t); ENSURE(charsWritten != 0); fprintf(f, "%ls\n\n", timestampBuf); } // OS fprintf(f, "OS : %s %s (%s)\n", un.sysname, un.release, un.version); // CPU fprintf(f, "CPU : %s, %s", un.machine, cpu_IdentifierString()); #if ARCH_X86_X64 fprintf(f, " (%dx%dx%d)", (int)topology::NumPackages(), (int)topology::CoresPerPackage(), (int)topology::LogicalPerCore()); #endif double cpuClock = os_cpu_ClockFrequency(); // query OS (may fail) #if ARCH_X86_X64 if(cpuClock <= 0.0) cpuClock = x86_x64::ClockFrequency(); // measure (takes a few ms) #endif if(cpuClock > 0.0) { if(cpuClock < 1e9) fprintf(f, ", %.2f MHz\n", cpuClock*1e-6); else fprintf(f, ", %.2f GHz\n", cpuClock*1e-9); } else fprintf(f, "\n"); // memory fprintf(f, "Memory : %u MiB; %u MiB free\n", (unsigned)os_cpu_MemorySize(), (unsigned)os_cpu_MemoryAvailable()); // graphics const std::wstring cardName = gfx::CardName(); const std::wstring driverInfo = gfx::DriverInfo(); fprintf(f, "Graphics Card : %ls\n", cardName.c_str()); fprintf(f, "OpenGL Drivers : %s; %ls\n", glGetString(GL_VERSION), driverInfo.c_str()); fprintf(f, "Video Mode : %dx%d:%d\n", g_VideoMode.GetXRes(), g_VideoMode.GetYRes(), g_VideoMode.GetBPP()); // sound fprintf(f, "Sound Card : %s\n", snd_card.c_str()); fprintf(f, "Sound Drivers : %s\n", snd_drv_ver.c_str()); // OpenGL extensions (write them last, since it's a lot of text) const char* exts = ogl_ExtensionString(); if (!exts) exts = "{unknown}"; fprintf(f, "\nOpenGL Extensions: \n%s\n", SplitExts(exts).c_str()); // System Management BIOS (even more text than OpenGL extensions) std::string smbios = SMBIOS::StringizeStructures(SMBIOS::GetStructures()); fprintf(f, "\nSMBIOS: \n%s\n", smbios.c_str()); fclose(f); f = 0; } // not thread-safe! static const wchar_t* HardcodedErrorString(int err) { static wchar_t description[200]; StatusDescription((Status)err, description, ARRAY_SIZE(description)); return description; } // not thread-safe! const wchar_t* ErrorString(int err) { // language file not available (yet) return HardcodedErrorString(err); // TODO: load from language file } // write the specified texture to disk. // note: cannot be made const because the image may have to be // transformed to write it out in the format determined by 's extension. Status tex_write(Tex* t, const VfsPath& filename) { DynArray da; RETURN_STATUS_IF_ERR(t->encode(filename.Extension(), &da)); // write to disk Status ret = INFO::OK; { shared_ptr file = DummySharedPtr(da.base); const ssize_t bytes_written = g_VFS->CreateFile(filename, file, da.pos); if(bytes_written > 0) ENSURE(bytes_written == (ssize_t)da.pos); else ret = (Status)bytes_written; } (void)da_free(&da); return ret; } /** * Return an unused directory, based on date and index (for example 2016-02-09_0001) */ OsPath createDateIndexSubdirectory(const OsPath& parentDir) { const std::time_t timestamp = std::time(nullptr); const struct std::tm* now = std::localtime(×tamp); // Two processes executing this simultaneously might attempt to create the same directory. int tries = 0; const int maxTries = 10; int i = 0; OsPath path; char directory[256]; do { sprintf(directory, "%04d-%02d-%02d_%04d", now->tm_year+1900, now->tm_mon+1, now->tm_mday, ++i); path = parentDir / CStr(directory); if (DirectoryExists(path) || FileExists(path)) continue; if (CreateDirectories(path, 0700, ++tries > maxTries) == INFO::OK) break; } while(tries <= maxTries); return path; } static size_t s_nextScreenshotNumber; // identifies the file format that is to be written // (case-insensitive). examples: "bmp", "png", "jpg". // BMP is good for quick output at the expense of large files. void WriteScreenshot(const VfsPath& extension) { // get next available numbered filename // note: %04d -> always 4 digits, so sorting by filename works correctly. const VfsPath basenameFormat(L"screenshots/screenshot%04d"); const VfsPath filenameFormat = basenameFormat.ChangeExtension(extension); VfsPath filename; vfs::NextNumberedFilename(g_VFS, filenameFormat, s_nextScreenshotNumber, filename); const size_t w = (size_t)g_xres, h = (size_t)g_yres; const size_t bpp = 24; GLenum fmt = GL_RGB; int flags = TEX_BOTTOM_UP; // we want writing BMP to be as fast as possible, // so read data from OpenGL in BMP format to obviate conversion. if(extension == L".bmp") { #if !CONFIG2_GLES // GLES doesn't support BGR fmt = GL_BGR; flags |= TEX_BGR; #endif } // Hide log messages and re-render RenderLogger(false); Render(); RenderLogger(true); const size_t img_size = w * h * bpp/8; const size_t hdr_size = tex_hdr_size(filename); shared_ptr buf; AllocateAligned(buf, hdr_size+img_size, maxSectorSize); GLvoid* img = buf.get() + hdr_size; Tex t; if(t.wrap(w, h, bpp, flags, buf, hdr_size) < 0) return; glReadPixels(0, 0, (GLsizei)w, (GLsizei)h, fmt, GL_UNSIGNED_BYTE, img); if (tex_write(&t, filename) == INFO::OK) { OsPath realPath; g_VFS->GetRealPath(filename, realPath); LOGMESSAGERENDER(g_L10n.Translate("Screenshot written to '%s'"), realPath.string8()); debug_printf( CStr(g_L10n.Translate("Screenshot written to '%s'") + "\n").c_str(), realPath.string8().c_str()); } else LOGERROR("Error writing screenshot to '%s'", filename.string8()); } // Similar to WriteScreenshot, but generates an image of size 640*tiles x 480*tiles. void WriteBigScreenshot(const VfsPath& extension, int tiles) { // If the game hasn't started yet then use WriteScreenshot to generate the image. if(g_Game == NULL){ WriteScreenshot(L".bmp"); return; } // get next available numbered filename // note: %04d -> always 4 digits, so sorting by filename works correctly. const VfsPath basenameFormat(L"screenshots/screenshot%04d"); const VfsPath filenameFormat = basenameFormat.ChangeExtension(extension); VfsPath filename; vfs::NextNumberedFilename(g_VFS, filenameFormat, s_nextScreenshotNumber, filename); // Slightly ugly and inflexible: Always draw 640*480 tiles onto the screen, and // hope the screen is actually large enough for that. const int tile_w = 640, tile_h = 480; ENSURE(g_xres >= tile_w && g_yres >= tile_h); const int img_w = tile_w*tiles, img_h = tile_h*tiles; const int bpp = 24; GLenum fmt = GL_RGB; int flags = TEX_BOTTOM_UP; // we want writing BMP to be as fast as possible, // so read data from OpenGL in BMP format to obviate conversion. if(extension == L".bmp") { #if !CONFIG2_GLES // GLES doesn't support BGR fmt = GL_BGR; flags |= TEX_BGR; #endif } const size_t img_size = img_w * img_h * bpp/8; const size_t tile_size = tile_w * tile_h * bpp/8; const size_t hdr_size = tex_hdr_size(filename); void* tile_data = malloc(tile_size); if(!tile_data) { WARN_IF_ERR(ERR::NO_MEM); return; } shared_ptr img_buf; AllocateAligned(img_buf, hdr_size+img_size, maxSectorSize); Tex t; GLvoid* img = img_buf.get() + hdr_size; if(t.wrap(img_w, img_h, bpp, flags, img_buf, hdr_size) < 0) { free(tile_data); return; } ogl_WarnIfError(); CCamera oldCamera = *g_Game->GetView()->GetCamera(); // Resize various things so that the sizes and aspect ratios are correct { g_Renderer.Resize(tile_w, tile_h); SViewPort vp = { 0, 0, tile_w, tile_h }; g_Game->GetView()->SetViewport(vp); } #if !CONFIG2_GLES // Temporarily move everything onto the front buffer, so the user can // see the exciting progress as it renders (and can tell when it's finished). // (It doesn't just use SwapBuffers, because it doesn't know whether to // call the SDL version or the Atlas version.) GLint oldReadBuffer, oldDrawBuffer; glGetIntegerv(GL_READ_BUFFER, &oldReadBuffer); glGetIntegerv(GL_DRAW_BUFFER, &oldDrawBuffer); glDrawBuffer(GL_FRONT); glReadBuffer(GL_FRONT); #endif // Hide the cursor CStrW oldCursor = g_CursorName; g_CursorName = L""; // Render each tile CMatrix3D projection; projection.SetIdentity(); for (int tile_y = 0; tile_y < tiles; ++tile_y) { for (int tile_x = 0; tile_x < tiles; ++tile_x) { // Adjust the camera to render the appropriate region if (oldCamera.GetProjectionType() == CCamera::PERSPECTIVE) { projection.SetPerspectiveTile(oldCamera.GetFOV(), oldCamera.GetAspectRatio(), oldCamera.GetNearPlane(), oldCamera.GetFarPlane(), tiles, tile_x, tile_y); } g_Game->GetView()->GetCamera()->SetProjection(projection); RenderLogger(false); RenderGui(false); Render(); RenderGui(true); RenderLogger(true); // Copy the tile pixels into the main image glReadPixels(0, 0, tile_w, tile_h, fmt, GL_UNSIGNED_BYTE, tile_data); for (int y = 0; y < tile_h; ++y) { void* dest = (char*)img + ((tile_y*tile_h + y) * img_w + (tile_x*tile_w)) * bpp/8; void* src = (char*)tile_data + y * tile_w * bpp/8; memcpy(dest, src, tile_w * bpp/8); } } } // Restore the old cursor g_CursorName = oldCursor; #if !CONFIG2_GLES // Restore the buffer settings glDrawBuffer(oldDrawBuffer); glReadBuffer(oldReadBuffer); #endif // Restore the viewport settings { g_Renderer.Resize(g_xres, g_yres); SViewPort vp = { 0, 0, g_xres, g_yres }; g_Game->GetView()->SetViewport(vp); g_Game->GetView()->GetCamera()->SetProjectionFromCamera(oldCamera); } if (tex_write(&t, filename) == INFO::OK) { OsPath realPath; g_VFS->GetRealPath(filename, realPath); LOGMESSAGERENDER(g_L10n.Translate("Screenshot written to '%s'"), realPath.string8()); debug_printf( CStr(g_L10n.Translate("Screenshot written to '%s'") + "\n").c_str(), realPath.string8().c_str()); } else LOGERROR("Error writing screenshot to '%s'", filename.string8()); free(tile_data); } std::string Hexify(const std::string& s) { std::stringstream str; str << std::hex; for (const char& c : s) str << std::setfill('0') << std::setw(2) << static_cast(static_cast(c)); return str.str(); } std::string Hexify(const u8* s, size_t length) { std::stringstream str; str << std::hex; for (size_t i = 0; i < length; ++i) str << std::setfill('0') << std::setw(2) << static_cast(s[i]); return str.str(); } Index: ps/trunk/source/ps/VisualReplay.h =================================================================== --- ps/trunk/source/ps/VisualReplay.h (revision 23320) +++ ps/trunk/source/ps/VisualReplay.h (revision 23321) @@ -1,123 +1,125 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_REPlAY #define INCLUDED_REPlAY +#include "lib/os_path.h" #include "scriptinterface/ScriptInterface.h" + class CSimulation2; class CGUIManager; /** * Contains functions for visually replaying past games. */ namespace VisualReplay { /** * Returns the absolute path to the sim-log directory (that contains the directories with the replay files. */ OsPath GetDirectoryPath(); /** * Returns the absolute path to the replay cache file. */ OsPath GetCacheFilePath(); /** * Returns the absolute path to the temporary replay cache file used to * always have a valid cache file in place even if bad things happen. */ OsPath GetTempCacheFilePath(); /** * Replays the commands.txt file in the given subdirectory visually. */ bool StartVisualReplay(const OsPath& directory); /** * Reads the replay Cache file and parses it into a jsObject * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param cachedReplaysObject - the cached replays. * @return true on succes */ bool ReadCacheFile(const ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject); /** * Stores the replay list in the replay cache file * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param replays - the replay list to store. */ void StoreCacheFile(const ScriptInterface& scriptInterface, JS::HandleObject replays); /** * Load the replay cache and check if there are new/deleted replays. If so, update the cache. * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param compareFiles - compare the directory name and the FileSize of the replays and the cache. * @return cache entries */ JS::HandleObject ReloadReplayCache(const ScriptInterface& scriptInterface, bool compareFiles); /** * Get a list of replays to display in the GUI. * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param compareFiles - reload the cache, which takes more time, * but nearly ensures, that no changed replay is missed. * @return array of objects containing replay data */ JS::Value GetReplays(const ScriptInterface& scriptInterface, bool compareFiles); /** * Parses a commands.txt file and extracts metadata. * Works similarly to CGame::LoadReplayData(). */ JS::Value LoadReplayData(const ScriptInterface& scriptInterface, const OsPath& directory); /** * Permanently deletes the visual replay (including the parent directory) * * @param replayFile - path to commands.txt, whose parent directory will be deleted. * @return true if deletion was successful, false on error */ bool DeleteReplay(const OsPath& replayFile); /** * Returns the parsed header of the replay file (commands.txt). */ JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const OsPath& directoryName); /** * Returns whether or not the metadata / summary screen data has been saved properly when the game ended. */ bool HasReplayMetadata(const OsPath& directoryName); /** * Returns the metadata of a replay. */ JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const OsPath& directoryName); /** * Adds a replay to the replayCache. */ void AddReplayToCache(const ScriptInterface& scriptInterface, const CStrW& directoryName); } #endif