Civ backgrounds for minimap
[0ad.git] / source / ps / UserReport.cpp
blob73805be9d36f8a24f967b78b5596e8407d4e7fd2
1 /* Copyright (C) 2022 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include "UserReport.h"
22 #include "lib/timer.h"
23 #include "lib/utf8.h"
24 #include "lib/external_libraries/curl.h"
25 #include "lib/external_libraries/zlib.h"
26 #include "lib/file/archive/stream.h"
27 #include "lib/os_path.h"
28 #include "lib/sysdep/sysdep.h"
29 #include "ps/ConfigDB.h"
30 #include "ps/Filesystem.h"
31 #include "ps/Profiler2.h"
32 #include "ps/Pyrogenesis.h"
33 #include "ps/Threading.h"
35 #include <condition_variable>
36 #include <deque>
37 #include <fstream>
38 #include <mutex>
39 #include <string>
40 #include <thread>
42 #define DEBUG_UPLOADS 0
45 * The basic idea is that the game submits reports to us, which we send over
46 * HTTP to a server for storage and analysis.
48 * We can't use libcurl's asynchronous 'multi' API, because DNS resolution can
49 * be synchronous and slow (which would make the game pause).
50 * So we use the 'easy' API in a background thread.
51 * The main thread submits reports, toggles whether uploading is enabled,
52 * and polls for the current status (typically to display in the GUI);
53 * the worker thread does all of the uploading.
55 * It'd be nice to extend this in the future to handle things like crash reports.
56 * The game should store the crashlogs (suitably anonymised) in a directory, and
57 * we should detect those files and upload them when we're restarted and online.
61 /**
62 * Version number stored in config file when the user agrees to the reporting.
63 * Reporting will be disabled if the config value is missing or is less than
64 * this value. If we start reporting a lot more data, we should increase this
65 * value and get the user to re-confirm.
67 static const int REPORTER_VERSION = 1;
69 /**
70 * Time interval (seconds) at which the worker thread will check its reconnection
71 * timers. (This should be relatively high so the thread doesn't waste much time
72 * continually waking up.)
74 static const double TIMER_CHECK_INTERVAL = 10.0;
76 /**
77 * Seconds we should wait before reconnecting to the server after a failure.
79 static const double RECONNECT_INVERVAL = 60.0;
81 CUserReporter g_UserReporter;
83 struct CUserReport
85 time_t m_Time;
86 std::string m_Type;
87 int m_Version;
88 std::string m_Data;
91 class CUserReporterWorker
93 public:
94 CUserReporterWorker(const std::string& userID, const std::string& url) :
95 m_URL(url), m_UserID(userID), m_Enabled(false), m_Shutdown(false), m_Status("disabled"),
96 m_PauseUntilTime(timer_Time()), m_LastUpdateTime(timer_Time())
98 // Set up libcurl:
100 m_Curl = curl_easy_init();
101 ENSURE(m_Curl);
103 #if DEBUG_UPLOADS
104 curl_easy_setopt(m_Curl, CURLOPT_VERBOSE, 1L);
105 #endif
107 // Capture error messages
108 curl_easy_setopt(m_Curl, CURLOPT_ERRORBUFFER, m_ErrorBuffer);
110 // Disable signal handlers (required for multithreaded applications)
111 curl_easy_setopt(m_Curl, CURLOPT_NOSIGNAL, 1L);
113 // To minimise security risks, don't support redirects
114 curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L);
116 // Prevent this thread from blocking the engine shutdown for 5 minutes in case the server is unavailable
117 curl_easy_setopt(m_Curl, CURLOPT_CONNECTTIMEOUT, 10L);
119 // Set IO callbacks
120 curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, ReceiveCallback);
121 curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, this);
122 curl_easy_setopt(m_Curl, CURLOPT_READFUNCTION, SendCallback);
123 curl_easy_setopt(m_Curl, CURLOPT_READDATA, this);
125 // Set URL to POST to
126 curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str());
127 curl_easy_setopt(m_Curl, CURLOPT_POST, 1L);
129 // Set up HTTP headers
130 m_Headers = NULL;
131 // Set the UA string
132 std::string ua = "User-Agent: 0ad ";
133 ua += curl_version();
134 ua += " (http://play0ad.com/)";
135 m_Headers = curl_slist_append(m_Headers, ua.c_str());
136 // Override the default application/x-www-form-urlencoded type since we're not using that type
137 m_Headers = curl_slist_append(m_Headers, "Content-Type: application/octet-stream");
138 // Disable the Accept header because it's a waste of a dozen bytes
139 m_Headers = curl_slist_append(m_Headers, "Accept: ");
140 curl_easy_setopt(m_Curl, CURLOPT_HTTPHEADER, m_Headers);
142 m_WorkerThread = std::thread(Threading::HandleExceptions<RunThread>::Wrapper, this);
145 ~CUserReporterWorker()
147 curl_slist_free_all(m_Headers);
148 curl_easy_cleanup(m_Curl);
152 * Called by main thread, when the online reporting is enabled/disabled.
154 void SetEnabled(bool enabled)
156 std::lock_guard<std::mutex> lock(m_WorkerMutex);
157 if (enabled != m_Enabled)
159 m_Enabled = enabled;
161 // Wake up the worker thread
162 m_WorkerCV.notify_all();
167 * Called by main thread to request shutdown.
168 * Returns true if we've shut down successfully.
169 * Returns false if shutdown is taking too long (we might be blocked on a
170 * sync network operation) - you mustn't destroy this object, just leak it
171 * and terminate.
173 bool Shutdown()
176 std::lock_guard<std::mutex> lock(m_WorkerMutex);
177 m_Shutdown = true;
180 // Wake up the worker thread
181 m_WorkerCV.notify_all();
183 // Wait for it to shut down cleanly
184 // TODO: should have a timeout in case of network hangs
185 m_WorkerThread.join();
187 return true;
191 * Called by main thread to determine the current status of the uploader.
193 std::string GetStatus()
195 std::lock_guard<std::mutex> lock(m_WorkerMutex);
196 return m_Status;
200 * Called by main thread to add a new report to the queue.
202 void Submit(const std::shared_ptr<CUserReport>& report)
205 std::lock_guard<std::mutex> lock(m_WorkerMutex);
206 m_ReportQueue.push_back(report);
209 // Wake up the worker thread
210 m_WorkerCV.notify_all();
214 * Called by the main thread every frame, so we can check
215 * retransmission timers.
217 void Update()
219 double now = timer_Time();
220 if (now > m_LastUpdateTime + TIMER_CHECK_INTERVAL)
222 // Wake up the worker thread
223 m_WorkerCV.notify_all();
225 m_LastUpdateTime = now;
229 private:
230 static void RunThread(CUserReporterWorker* data)
232 debug_SetThreadName("CUserReportWorker");
233 g_Profiler2.RegisterCurrentThread("userreport");
235 data->Run();
238 void Run()
240 // Set libcurl's proxy configuration
241 // (This has to be done in the thread because it's potentially very slow)
242 SetStatus("proxy");
243 std::wstring proxy;
246 PROFILE2("get proxy config");
247 if (sys_get_proxy_config(wstring_from_utf8(m_URL), proxy) == INFO::OK)
248 curl_easy_setopt(m_Curl, CURLOPT_PROXY, utf8_from_wstring(proxy).c_str());
251 SetStatus("waiting");
254 * We use a condition_variable to let the thread be woken up when it has
255 * work to do. Various actions from the main thread can wake it:
256 * * SetEnabled()
257 * * Shutdown()
258 * * Submit()
259 * * Retransmission timeouts, once every several seconds
261 * If multiple actions have triggered wakeups, we might respond to
262 * all of those actions after the first wakeup, which is okay (we'll do
263 * nothing during the subsequent wakeups). We should never hang due to
264 * processing fewer actions than wakeups.
266 * Retransmission timeouts are triggered via the main thread.
269 // Wait until the main thread wakes us up
270 while (true)
272 g_Profiler2.RecordRegionEnter("condition_variable wait");
274 std::unique_lock<std::mutex> lock(m_WorkerMutex);
275 m_WorkerCV.wait(lock);
276 lock.unlock();
278 g_Profiler2.RecordRegionLeave();
280 // Handle shutdown requests as soon as possible
281 if (GetShutdown())
282 return;
284 // If we're not enabled, ignore this wakeup
285 if (!GetEnabled())
286 continue;
288 // If we're still pausing due to a failed connection,
289 // go back to sleep again
290 if (timer_Time() < m_PauseUntilTime)
291 continue;
293 // We're enabled, so process as many reports as possible
294 while (ProcessReport())
296 // Handle shutdowns while we were sending the report
297 if (GetShutdown())
298 return;
303 bool GetEnabled()
305 std::lock_guard<std::mutex> lock(m_WorkerMutex);
306 return m_Enabled;
309 bool GetShutdown()
311 std::lock_guard<std::mutex> lock(m_WorkerMutex);
312 return m_Shutdown;
315 void SetStatus(const std::string& status)
317 std::lock_guard<std::mutex> lock(m_WorkerMutex);
318 m_Status = status;
319 #if DEBUG_UPLOADS
320 debug_printf(">>> CUserReporterWorker status: %s\n", status.c_str());
321 #endif
324 bool ProcessReport()
326 PROFILE2("process report");
328 std::shared_ptr<CUserReport> report;
331 std::lock_guard<std::mutex> lock(m_WorkerMutex);
332 if (m_ReportQueue.empty())
333 return false;
334 report = m_ReportQueue.front();
335 m_ReportQueue.pop_front();
338 ConstructRequestData(*report);
339 m_RequestDataOffset = 0;
340 m_ResponseData.clear();
341 m_ErrorBuffer[0] = '\0';
343 curl_easy_setopt(m_Curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)m_RequestData.size());
345 SetStatus("connecting");
347 #if DEBUG_UPLOADS
348 TIMER(L"CUserReporterWorker request");
349 #endif
351 CURLcode err = curl_easy_perform(m_Curl);
353 #if DEBUG_UPLOADS
354 printf(">>>\n%s\n<<<\n", m_ResponseData.c_str());
355 #endif
357 if (err == CURLE_OK)
359 long code = -1;
360 curl_easy_getinfo(m_Curl, CURLINFO_RESPONSE_CODE, &code);
361 SetStatus("completed:" + CStr::FromInt(code));
363 // Check for success code
364 if (code == 200)
365 return true;
367 // If the server returns the 410 Gone status, interpret that as meaning
368 // it no longer supports uploads (at least from this version of the game),
369 // so shut down and stop talking to it (to avoid wasting bandwidth)
370 if (code == 410)
372 std::lock_guard<std::mutex> lock(m_WorkerMutex);
373 m_Shutdown = true;
374 return false;
377 else
379 std::string errorString(m_ErrorBuffer);
381 if (errorString.empty())
382 errorString = curl_easy_strerror(err);
384 SetStatus("failed:" + CStr::FromInt(err) + ":" + errorString);
387 // We got an unhandled return code or a connection failure;
388 // push this report back onto the queue and try again after
389 // a long interval
392 std::lock_guard<std::mutex> lock(m_WorkerMutex);
393 m_ReportQueue.push_front(report);
396 m_PauseUntilTime = timer_Time() + RECONNECT_INVERVAL;
397 return false;
400 void ConstructRequestData(const CUserReport& report)
402 // Construct the POST request data in the application/x-www-form-urlencoded format
404 std::string r;
406 r += "user_id=";
407 AppendEscaped(r, m_UserID);
409 r += "&time=" + CStr::FromInt64(report.m_Time);
411 r += "&type=";
412 AppendEscaped(r, report.m_Type);
414 r += "&version=" + CStr::FromInt(report.m_Version);
416 r += "&data=";
417 AppendEscaped(r, report.m_Data);
419 // Compress the content with zlib to save bandwidth.
420 // (Note that we send a request with unlabelled compressed data instead
421 // of using Content-Encoding, because Content-Encoding is a mess and causes
422 // problems with servers and breaks Content-Length and this is much easier.)
423 std::string compressed;
424 compressed.resize(compressBound(r.size()));
425 uLongf destLen = compressed.size();
426 int ok = compress((Bytef*)compressed.c_str(), &destLen, (const Bytef*)r.c_str(), r.size());
427 ENSURE(ok == Z_OK);
428 compressed.resize(destLen);
430 m_RequestData.swap(compressed);
433 void AppendEscaped(std::string& buffer, const std::string& str)
435 char* escaped = curl_easy_escape(m_Curl, str.c_str(), str.size());
436 buffer += escaped;
437 curl_free(escaped);
440 static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp)
442 CUserReporterWorker* self = static_cast<CUserReporterWorker*>(userp);
444 if (self->GetShutdown())
445 return 0; // signals an error
447 self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb);
449 return size*nmemb;
452 static size_t SendCallback(char* bufptr, size_t size, size_t nmemb, void* userp)
454 CUserReporterWorker* self = static_cast<CUserReporterWorker*>(userp);
456 if (self->GetShutdown())
457 return CURL_READFUNC_ABORT; // signals an error
459 // We can return as much data as available, up to the buffer size
460 size_t amount = std::min(self->m_RequestData.size() - self->m_RequestDataOffset, size*nmemb);
462 // ...But restrict to sending a small amount at once, so that we remain
463 // responsive to shutdown requests even if the network is pretty slow
464 amount = std::min((size_t)1024, amount);
466 if(amount != 0) // (avoids invalid operator[] call where index=size)
468 memcpy(bufptr, &self->m_RequestData[self->m_RequestDataOffset], amount);
469 self->m_RequestDataOffset += amount;
472 self->SetStatus("sending:" + CStr::FromDouble((double)self->m_RequestDataOffset / self->m_RequestData.size()));
474 return amount;
477 private:
478 // Thread-related members:
479 std::thread m_WorkerThread;
480 std::mutex m_WorkerMutex;
481 std::condition_variable m_WorkerCV;
483 // Shared by main thread and worker thread:
484 // These variables are all protected by m_WorkerMutex
485 std::deque<std::shared_ptr<CUserReport>> m_ReportQueue;
486 bool m_Enabled;
487 bool m_Shutdown;
488 std::string m_Status;
490 // Initialised in constructor by main thread; otherwise used only by worker thread:
491 std::string m_URL;
492 std::string m_UserID;
493 CURL* m_Curl;
494 curl_slist* m_Headers;
495 double m_PauseUntilTime;
497 // Only used by worker thread:
498 std::string m_ResponseData;
499 std::string m_RequestData;
500 size_t m_RequestDataOffset;
501 char m_ErrorBuffer[CURL_ERROR_SIZE];
503 // Only used by main thread:
504 double m_LastUpdateTime;
509 CUserReporter::CUserReporter() :
510 m_Worker(NULL)
514 CUserReporter::~CUserReporter()
516 ENSURE(!m_Worker); // Deinitialize should have been called before shutdown
519 std::string CUserReporter::LoadUserID()
521 std::string userID;
523 // Read the user ID from user.cfg (if there is one)
524 CFG_GET_VAL("userreport.id", userID);
526 // If we don't have a validly-formatted user ID, generate a new one
527 if (userID.length() != 16)
529 u8 bytes[8] = {0};
530 sys_generate_random_bytes(bytes, ARRAY_SIZE(bytes));
531 // ignore failures - there's not much we can do about it
533 userID = "";
534 for (size_t i = 0; i < ARRAY_SIZE(bytes); ++i)
536 char hex[3];
537 sprintf_s(hex, ARRAY_SIZE(hex), "%02x", (unsigned int)bytes[i]);
538 userID += hex;
541 g_ConfigDB.SetValueString(CFG_USER, "userreport.id", userID);
542 g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.id", userID);
545 return userID;
548 bool CUserReporter::IsReportingEnabled()
550 int version = -1;
551 CFG_GET_VAL("userreport.enabledversion", version);
552 return (version >= REPORTER_VERSION);
555 void CUserReporter::SetReportingEnabled(bool enabled)
557 CStr val = CStr::FromInt(enabled ? REPORTER_VERSION : 0);
558 g_ConfigDB.SetValueString(CFG_USER, "userreport.enabledversion", val);
559 g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.enabledversion", val);
561 if (m_Worker)
562 m_Worker->SetEnabled(enabled);
565 std::string CUserReporter::GetStatus()
567 if (!m_Worker)
568 return "disabled";
570 return m_Worker->GetStatus();
573 void CUserReporter::Initialize()
575 ENSURE(!m_Worker); // must only be called once
577 std::string userID = LoadUserID();
578 std::string url;
579 CFG_GET_VAL("userreport.url_upload", url);
581 m_Worker = new CUserReporterWorker(userID, url);
583 m_Worker->SetEnabled(IsReportingEnabled());
586 void CUserReporter::Deinitialize()
588 if (!m_Worker)
589 return;
591 if (m_Worker->Shutdown())
593 // Worker was shut down cleanly
594 SAFE_DELETE(m_Worker);
596 else
598 // Worker failed to shut down in a reasonable time
599 // Leak the resources (since that's better than hanging or crashing)
600 m_Worker = NULL;
604 void CUserReporter::Update()
606 if (m_Worker)
607 m_Worker->Update();
610 void CUserReporter::SubmitReport(const std::string& type, int version, const std::string& data, const std::string& dataHumanReadable)
612 // Write to logfile, enabling users to assess privacy concerns before the data is submitted
613 if (!dataHumanReadable.empty())
615 OsPath path = psLogDir() / OsPath("userreport_" + type + ".txt");
616 std::ofstream stream(OsString(path), std::ofstream::trunc);
617 if (stream)
619 debug_printf("FILES| UserReport written to '%s'\n", path.string8().c_str());
620 stream << dataHumanReadable << std::endl;
621 stream.close();
623 else
624 debug_printf("FILES| Failed to write UserReport to '%s'\n", path.string8().c_str());
627 // If not initialised, discard the report
628 if (!m_Worker)
629 return;
631 // Actual submit
632 std::shared_ptr<CUserReport> report = std::make_shared<CUserReport>();
633 report->m_Time = time(NULL);
634 report->m_Type = type;
635 report->m_Version = version;
636 report->m_Data = data;
638 m_Worker->Submit(report);