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"
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>
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.
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;
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;
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
;
91 class CUserReporterWorker
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())
100 m_Curl
= curl_easy_init();
104 curl_easy_setopt(m_Curl
, CURLOPT_VERBOSE
, 1L);
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);
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
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
)
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
176 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
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();
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
);
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.
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
;
230 static void RunThread(CUserReporterWorker
* data
)
232 debug_SetThreadName("CUserReportWorker");
233 g_Profiler2
.RegisterCurrentThread("userreport");
240 // Set libcurl's proxy configuration
241 // (This has to be done in the thread because it's potentially very slow)
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:
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
272 g_Profiler2
.RecordRegionEnter("condition_variable wait");
274 std::unique_lock
<std::mutex
> lock(m_WorkerMutex
);
275 m_WorkerCV
.wait(lock
);
278 g_Profiler2
.RecordRegionLeave();
280 // Handle shutdown requests as soon as possible
284 // If we're not enabled, ignore this wakeup
288 // If we're still pausing due to a failed connection,
289 // go back to sleep again
290 if (timer_Time() < m_PauseUntilTime
)
293 // We're enabled, so process as many reports as possible
294 while (ProcessReport())
296 // Handle shutdowns while we were sending the report
305 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
311 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
315 void SetStatus(const std::string
& status
)
317 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
320 debug_printf(">>> CUserReporterWorker status: %s\n", status
.c_str());
326 PROFILE2("process report");
328 std::shared_ptr
<CUserReport
> report
;
331 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
332 if (m_ReportQueue
.empty())
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");
348 TIMER(L
"CUserReporterWorker request");
351 CURLcode err
= curl_easy_perform(m_Curl
);
354 printf(">>>\n%s\n<<<\n", m_ResponseData
.c_str());
360 curl_easy_getinfo(m_Curl
, CURLINFO_RESPONSE_CODE
, &code
);
361 SetStatus("completed:" + CStr::FromInt(code
));
363 // Check for success code
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)
372 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
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
392 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
393 m_ReportQueue
.push_front(report
);
396 m_PauseUntilTime
= timer_Time() + RECONNECT_INVERVAL
;
400 void ConstructRequestData(const CUserReport
& report
)
402 // Construct the POST request data in the application/x-www-form-urlencoded format
407 AppendEscaped(r
, m_UserID
);
409 r
+= "&time=" + CStr::FromInt64(report
.m_Time
);
412 AppendEscaped(r
, report
.m_Type
);
414 r
+= "&version=" + CStr::FromInt(report
.m_Version
);
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());
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());
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
);
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()));
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
;
488 std::string m_Status
;
490 // Initialised in constructor by main thread; otherwise used only by worker thread:
492 std::string m_UserID
;
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() :
514 CUserReporter::~CUserReporter()
516 ENSURE(!m_Worker
); // Deinitialize should have been called before shutdown
519 std::string
CUserReporter::LoadUserID()
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)
530 sys_generate_random_bytes(bytes
, ARRAY_SIZE(bytes
));
531 // ignore failures - there's not much we can do about it
534 for (size_t i
= 0; i
< ARRAY_SIZE(bytes
); ++i
)
537 sprintf_s(hex
, ARRAY_SIZE(hex
), "%02x", (unsigned int)bytes
[i
]);
541 g_ConfigDB
.SetValueString(CFG_USER
, "userreport.id", userID
);
542 g_ConfigDB
.WriteValueToFile(CFG_USER
, "userreport.id", userID
);
548 bool CUserReporter::IsReportingEnabled()
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
);
562 m_Worker
->SetEnabled(enabled
);
565 std::string
CUserReporter::GetStatus()
570 return m_Worker
->GetStatus();
573 void CUserReporter::Initialize()
575 ENSURE(!m_Worker
); // must only be called once
577 std::string userID
= LoadUserID();
579 CFG_GET_VAL("userreport.url_upload", url
);
581 m_Worker
= new CUserReporterWorker(userID
, url
);
583 m_Worker
->SetEnabled(IsReportingEnabled());
586 void CUserReporter::Deinitialize()
591 if (m_Worker
->Shutdown())
593 // Worker was shut down cleanly
594 SAFE_DELETE(m_Worker
);
598 // Worker failed to shut down in a reasonable time
599 // Leak the resources (since that's better than hanging or crashing)
604 void CUserReporter::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
);
619 debug_printf("FILES| UserReport written to '%s'\n", path
.string8().c_str());
620 stream
<< dataHumanReadable
<< std::endl
;
624 debug_printf("FILES| Failed to write UserReport to '%s'\n", path
.string8().c_str());
627 // If not initialised, discard the report
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
);