Pyrogenesis  13997
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
UserReport.cpp
Go to the documentation of this file.
1 /* Copyright (C) 2012 Wildfire Games.
2  * This file is part of 0 A.D.
3  *
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.
8  *
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.
13  *
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/>.
16  */
17 
18 #include "precompiled.h"
19 
20 #include "UserReport.h"
21 
22 #include "lib/timer.h"
23 #include "lib/utf8.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/ThreadUtil.h"
33 
34 #define DEBUG_UPLOADS 0
35 
36 /*
37  * The basic idea is that the game submits reports to us, which we send over
38  * HTTP to a server for storage and analysis.
39  *
40  * We can't use libcurl's asynchronous 'multi' API, because DNS resolution can
41  * be synchronous and slow (which would make the game pause).
42  * So we use the 'easy' API in a background thread.
43  * The main thread submits reports, toggles whether uploading is enabled,
44  * and polls for the current status (typically to display in the GUI);
45  * the worker thread does all of the uploading.
46  *
47  * It'd be nice to extend this in the future to handle things like crash reports.
48  * The game should store the crashlogs (suitably anonymised) in a directory, and
49  * we should detect those files and upload them when we're restarted and online.
50  */
51 
52 
53 /**
54  * Version number stored in config file when the user agrees to the reporting.
55  * Reporting will be disabled if the config value is missing or is less than
56  * this value. If we start reporting a lot more data, we should increase this
57  * value and get the user to re-confirm.
58  */
59 static const int REPORTER_VERSION = 1;
60 
61 /**
62  * Time interval (seconds) at which the worker thread will check its reconnection
63  * timers. (This should be relatively high so the thread doesn't waste much time
64  * continually waking up.)
65  */
66 static const double TIMER_CHECK_INTERVAL = 10.0;
67 
68 /**
69  * Seconds we should wait before reconnecting to the server after a failure.
70  */
71 static const double RECONNECT_INVERVAL = 60.0;
72 
74 
76 {
77  time_t m_Time;
78  std::string m_Type;
79  int m_Version;
80  std::string m_Data;
81 };
82 
84 {
85 public:
86  CUserReporterWorker(const std::string& userID, const std::string& url) :
87  m_URL(url), m_UserID(userID), m_Enabled(false), m_Shutdown(false), m_Status("disabled"),
89  {
90  // Set up libcurl:
91 
92  m_Curl = curl_easy_init();
93  ENSURE(m_Curl);
94 
95 #if DEBUG_UPLOADS
96  curl_easy_setopt(m_Curl, CURLOPT_VERBOSE, 1L);
97 #endif
98 
99  // Capture error messages
100  curl_easy_setopt(m_Curl, CURLOPT_ERRORBUFFER, m_ErrorBuffer);
101 
102  // Disable signal handlers (required for multithreaded applications)
103  curl_easy_setopt(m_Curl, CURLOPT_NOSIGNAL, 1L);
104 
105  // To minimise security risks, don't support redirects
106  curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L);
107 
108  // Set IO callbacks
109  curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, ReceiveCallback);
110  curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, this);
111  curl_easy_setopt(m_Curl, CURLOPT_READFUNCTION, SendCallback);
112  curl_easy_setopt(m_Curl, CURLOPT_READDATA, this);
113 
114  // Set URL to POST to
115  curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str());
116  curl_easy_setopt(m_Curl, CURLOPT_POST, 1L);
117 
118  // Set up HTTP headers
119  m_Headers = NULL;
120  // Set the UA string
121  std::string ua = "User-Agent: 0ad ";
122  ua += curl_version();
123  ua += " (http://play0ad.com/)";
124  m_Headers = curl_slist_append(m_Headers, ua.c_str());
125  // Override the default application/x-www-form-urlencoded type since we're not using that type
126  m_Headers = curl_slist_append(m_Headers, "Content-Type: application/octet-stream");
127  // Disable the Accept header because it's a waste of a dozen bytes
128  m_Headers = curl_slist_append(m_Headers, "Accept: ");
129  curl_easy_setopt(m_Curl, CURLOPT_HTTPHEADER, m_Headers);
130 
131 
132  // Set up the worker thread:
133 
134  // Use SDL semaphores since OS X doesn't implement sem_init
137 
138  int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
139  ENSURE(ret == 0);
140  }
141 
143  {
144  // Clean up resources
145 
147 
148  curl_slist_free_all(m_Headers);
149  curl_easy_cleanup(m_Curl);
150  }
151 
152  /**
153  * Called by main thread, when the online reporting is enabled/disabled.
154  */
155  void SetEnabled(bool enabled)
156  {
158  if (enabled != m_Enabled)
159  {
160  m_Enabled = enabled;
161 
162  // Wake up the worker thread
164  }
165  }
166 
167  /**
168  * Called by main thread to request shutdown.
169  * Returns true if we've shut down successfully.
170  * Returns false if shutdown is taking too long (we might be blocked on a
171  * sync network operation) - you mustn't destroy this object, just leak it
172  * and terminate.
173  */
174  bool Shutdown()
175  {
176  {
178  m_Shutdown = true;
179  }
180 
181  // Wake up the worker thread
183 
184  // Wait for it to shut down cleanly
185  // TODO: should have a timeout in case of network hangs
187 
188  return true;
189  }
190 
191  /**
192  * Called by main thread to determine the current status of the uploader.
193  */
194  std::string GetStatus()
195  {
197  return m_Status;
198  }
199 
200  /**
201  * Called by main thread to add a new report to the queue.
202  */
203  void Submit(const shared_ptr<CUserReport>& report)
204  {
205  {
207  m_ReportQueue.push_back(report);
208  }
209 
210  // Wake up the worker thread
212  }
213 
214  /**
215  * Called by the main thread every frame, so we can check
216  * retransmission timers.
217  */
218  void Update()
219  {
220  double now = timer_Time();
222  {
223  // Wake up the worker thread
225 
226  m_LastUpdateTime = now;
227  }
228  }
229 
230 private:
231  static void* RunThread(void* data)
232  {
233  debug_SetThreadName("CUserReportWorker");
234  g_Profiler2.RegisterCurrentThread("userreport");
235 
236  static_cast<CUserReporterWorker*>(data)->Run();
237 
238  return NULL;
239  }
240 
241  void Run()
242  {
243  // Set libcurl's proxy configuration
244  // (This has to be done in the thread because it's potentially very slow)
245  SetStatus("proxy");
246  std::wstring proxy;
247 
248  {
249  PROFILE2("get proxy config");
251  curl_easy_setopt(m_Curl, CURLOPT_PROXY, utf8_from_wstring(proxy).c_str());
252  }
253 
254  SetStatus("waiting");
255 
256  /*
257  * We use a semaphore to let the thread be woken up when it has
258  * work to do. Various actions from the main thread can wake it:
259  * * SetEnabled()
260  * * Shutdown()
261  * * Submit()
262  * * Retransmission timeouts, once every several seconds
263  *
264  * If multiple actions have triggered wakeups, we might respond to
265  * all of those actions after the first wakeup, which is okay (we'll do
266  * nothing during the subsequent wakeups). We should never hang due to
267  * processing fewer actions than wakeups.
268  *
269  * Retransmission timeouts are triggered via the main thread - we can't simply
270  * use SDL_SemWaitTimeout because on Linux it's implemented as an inefficient
271  * busy-wait loop, and we can't use a manual busy-wait with a long delay time
272  * because we'd lose responsiveness. So the main thread pings the worker
273  * occasionally so it can check its timer.
274  */
275 
276  g_Profiler2.RecordRegionEnter("semaphore wait");
277 
278  // Wait until the main thread wakes us up
279  while (SDL_SemWait(m_WorkerSem) == 0)
280  {
281  g_Profiler2.RecordRegionLeave("semaphore wait");
282 
283  // Handle shutdown requests as soon as possible
284  if (GetShutdown())
285  return;
286 
287  // If we're not enabled, ignore this wakeup
288  if (!GetEnabled())
289  continue;
290 
291  // If we're still pausing due to a failed connection,
292  // go back to sleep again
294  continue;
295 
296  // We're enabled, so process as many reports as possible
297  while (ProcessReport())
298  {
299  // Handle shutdowns while we were sending the report
300  if (GetShutdown())
301  return;
302  }
303  }
304 
305  g_Profiler2.RecordRegionLeave("semaphore wait");
306  }
307 
308  bool GetEnabled()
309  {
311  return m_Enabled;
312  }
313 
314  bool GetShutdown()
315  {
317  return m_Shutdown;
318  }
319 
320  void SetStatus(const std::string& status)
321  {
323  m_Status = status;
324 #if DEBUG_UPLOADS
325  debug_printf(L">>> CUserReporterWorker status: %hs\n", status.c_str());
326 #endif
327  }
328 
330  {
331  PROFILE2("process report");
332 
333  shared_ptr<CUserReport> report;
334 
335  {
337  if (m_ReportQueue.empty())
338  return false;
339  report = m_ReportQueue.front();
340  m_ReportQueue.pop_front();
341  }
342 
343  ConstructRequestData(*report);
345  m_ResponseData.clear();
346 
347  curl_easy_setopt(m_Curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)m_RequestData.size());
348 
349  SetStatus("connecting");
350 
351 #if DEBUG_UPLOADS
352  TIMER(L"CUserReporterWorker request");
353 #endif
354 
355  CURLcode err = curl_easy_perform(m_Curl);
356 
357 #if DEBUG_UPLOADS
358  printf(">>>\n%s\n<<<\n", m_ResponseData.c_str());
359 #endif
360 
361  if (err == CURLE_OK)
362  {
363  long code = -1;
364  curl_easy_getinfo(m_Curl, CURLINFO_RESPONSE_CODE, &code);
365  SetStatus("completed:" + CStr::FromInt(code));
366 
367  // Check for success code
368  if (code == 200)
369  return true;
370 
371  // If the server returns the 410 Gone status, interpret that as meaning
372  // it no longer supports uploads (at least from this version of the game),
373  // so shut down and stop talking to it (to avoid wasting bandwidth)
374  if (code == 410)
375  {
377  m_Shutdown = true;
378  return false;
379  }
380  }
381  else
382  {
383  SetStatus("failed:" + CStr::FromInt(err) + ":" + m_ErrorBuffer);
384  }
385 
386  // We got an unhandled return code or a connection failure;
387  // push this report back onto the queue and try again after
388  // a long interval
389 
390  {
392  m_ReportQueue.push_front(report);
393  }
394 
396  return false;
397  }
398 
399  void ConstructRequestData(const CUserReport& report)
400  {
401  // Construct the POST request data in the application/x-www-form-urlencoded format
402 
403  std::string r;
404 
405  r += "user_id=";
407 
408  r += "&time=" + CStr::FromInt64(report.m_Time);
409 
410  r += "&type=";
411  AppendEscaped(r, report.m_Type);
412 
413  r += "&version=" + CStr::FromInt(report.m_Version);
414 
415  r += "&data=";
416  AppendEscaped(r, report.m_Data);
417 
418  // Compress the content with zlib to save bandwidth.
419  // (Note that we send a request with unlabelled compressed data instead
420  // of using Content-Encoding, because Content-Encoding is a mess and causes
421  // problems with servers and breaks Content-Length and this is much easier.)
422  std::string compressed;
423  compressed.resize(compressBound(r.size()));
424  uLongf destLen = compressed.size();
425  int ok = compress((Bytef*)compressed.c_str(), &destLen, (const Bytef*)r.c_str(), r.size());
426  ENSURE(ok == Z_OK);
427  compressed.resize(destLen);
428 
429  m_RequestData.swap(compressed);
430  }
431 
432  void AppendEscaped(std::string& buffer, const std::string& str)
433  {
434  char* escaped = curl_easy_escape(m_Curl, str.c_str(), str.size());
435  buffer += escaped;
436  curl_free(escaped);
437  }
438 
439  static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp)
440  {
441  CUserReporterWorker* self = static_cast<CUserReporterWorker*>(userp);
442 
443  if (self->GetShutdown())
444  return 0; // signals an error
445 
446  self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb);
447 
448  return size*nmemb;
449  }
450 
451  static size_t SendCallback(char* bufptr, size_t size, size_t nmemb, void* userp)
452  {
453  CUserReporterWorker* self = static_cast<CUserReporterWorker*>(userp);
454 
455  if (self->GetShutdown())
456  return CURL_READFUNC_ABORT; // signals an error
457 
458  // We can return as much data as available, up to the buffer size
459  size_t amount = std::min(self->m_RequestData.size() - self->m_RequestDataOffset, size*nmemb);
460 
461  // ...But restrict to sending a small amount at once, so that we remain
462  // responsive to shutdown requests even if the network is pretty slow
463  amount = std::min((size_t)1024, amount);
464 
465  if(amount != 0) // (avoids invalid operator[] call where index=size)
466  {
467  memcpy(bufptr, &self->m_RequestData[self->m_RequestDataOffset], amount);
468  self->m_RequestDataOffset += amount;
469  }
470 
471  self->SetStatus("sending:" + CStr::FromDouble((double)self->m_RequestDataOffset / self->m_RequestData.size()));
472 
473  return amount;
474  }
475 
476 private:
477  // Thread-related members:
481 
482  // Shared by main thread and worker thread:
483  // These variables are all protected by m_WorkerMutex
484  std::deque<shared_ptr<CUserReport> > m_ReportQueue;
485  bool m_Enabled;
487  std::string m_Status;
488 
489  // Initialised in constructor by main thread; otherwise used only by worker thread:
490  std::string m_URL;
491  std::string m_UserID;
492  CURL* m_Curl;
493  curl_slist* m_Headers;
495 
496  // Only used by worker thread:
497  std::string m_ResponseData;
498  std::string m_RequestData;
500  char m_ErrorBuffer[CURL_ERROR_SIZE];
501 
502  // Only used by main thread:
504 };
505 
506 
507 
509  m_Worker(NULL)
510 {
511 }
512 
514 {
515  ENSURE(!m_Worker); // Deinitialize should have been called before shutdown
516 }
517 
519 {
520  std::string userID;
521 
522  // Read the user ID from user.cfg (if there is one)
523  CFG_GET_VAL("userreport.id", String, userID);
524 
525  // If we don't have a validly-formatted user ID, generate a new one
526  if (userID.length() != 16)
527  {
528  u8 bytes[8] = {0};
529  sys_generate_random_bytes(bytes, ARRAY_SIZE(bytes));
530  // ignore failures - there's not much we can do about it
531 
532  userID = "";
533  for (size_t i = 0; i < ARRAY_SIZE(bytes); ++i)
534  {
535  char hex[3];
536  sprintf_s(hex, ARRAY_SIZE(hex), "%02x", (unsigned int)bytes[i]);
537  userID += hex;
538  }
539 
540  g_ConfigDB.CreateValue(CFG_USER, "userreport.id")->m_String = userID;
541  g_ConfigDB.WriteFile(CFG_USER);
542  }
543 
544  return userID;
545 }
546 
548 {
549  int version = -1;
550  CFG_GET_VAL("userreport.enabledversion", Int, version);
551  return (version >= REPORTER_VERSION);
552 }
553 
555 {
556  CStr val = CStr::FromInt(enabled ? REPORTER_VERSION : 0);
557  g_ConfigDB.CreateValue(CFG_USER, "userreport.enabledversion")->m_String = val;
558  g_ConfigDB.WriteFile(CFG_USER);
559 
560  if (m_Worker)
561  m_Worker->SetEnabled(enabled);
562 }
563 
565 {
566  if (!m_Worker)
567  return "disabled";
568 
569  return m_Worker->GetStatus();
570 }
571 
572 
574 {
575  ENSURE(!m_Worker); // must only be called once
576 
577  std::string userID = LoadUserID();
578  std::string url;
579  CFG_GET_VAL("userreport.url", String, url);
580 
581  // Initialise everything except Win32 sockets (because our networking
582  // system already inits those)
583  curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32);
584 
585  m_Worker = new CUserReporterWorker(userID, url);
586 
588 }
589 
591 {
592  if (!m_Worker)
593  return;
594 
595  if (m_Worker->Shutdown())
596  {
597  // Worker was shut down cleanly
598 
600  curl_global_cleanup();
601  }
602  else
603  {
604  // Worker failed to shut down in a reasonable time
605  // Leak the resources (since that's better than hanging or crashing)
606  m_Worker = NULL;
607  }
608 }
609 
611 {
612  if (m_Worker)
613  m_Worker->Update();
614 }
615 
616 void CUserReporter::SubmitReport(const char* type, int version, const std::string& data)
617 {
618  // If not initialised, discard the report
619  if (!m_Worker)
620  return;
621 
622  shared_ptr<CUserReport> report(new CUserReport);
623  report->m_Time = time(NULL);
624  report->m_Type = type;
625  report->m_Version = version;
626  report->m_Data = data;
627 
628  m_Worker->Submit(report);
629 }
void SetStatus(const std::string &status)
Definition: UserReport.cpp:320
void SDL_sem
Definition: wsdl.h:109
void Update()
Must be called frequently (preferably every frame), to update some internal reconnection timers...
Definition: UserReport.cpp:610
#define u8
Definition: types.h:39
static const double TIMER_CHECK_INTERVAL
Time interval (seconds) at which the worker thread will check its reconnection timers.
Definition: UserReport.cpp:66
CUserReporter g_UserReporter
Definition: UserReport.cpp:73
static const double RECONNECT_INVERVAL
Seconds we should wait before reconnecting to the server after a failure.
Definition: UserReport.cpp:71
void AppendEscaped(std::string &buffer, const std::string &str)
Definition: UserReport.cpp:432
const Status OK
Definition: status.h:386
std::string m_RequestData
Definition: UserReport.cpp:498
std::string GetStatus()
Definition: UserReport.cpp:564
std::string utf8_from_wstring(const std::wstring &src, Status *err)
opposite of wstring_from_utf8
Definition: utf8.cpp:208
curl_slist * m_Headers
Definition: UserReport.cpp:493
#define CFG_GET_VAL(name, type, destination)
Definition: ConfigDB.h:147
std::string m_ResponseData
Definition: UserReport.cpp:497
CUserReporterWorker(const std::string &userID, const std::string &url)
Definition: UserReport.cpp:86
Locks a CMutex over this object&#39;s lifetime.
Definition: ThreadUtil.h:73
pthread_t m_WorkerThread
Definition: UserReport.cpp:478
CUserReporterWorker * m_Worker
Definition: UserReport.h:57
Status sys_get_proxy_config(const std::wstring &url, std::wstring &proxy)
get the proxy address for accessing the given HTTP URL.
Definition: unix.cpp:338
A non-recursive mutual exclusion lock.
Definition: ThreadUtil.h:45
std::deque< shared_ptr< CUserReport > > m_ReportQueue
Definition: UserReport.cpp:484
int sprintf_s(char *buf, size_t max_chars, const char *fmt,...) PRINTF_ARGS(3)
#define ARRAY_SIZE(name)
LIB_API void debug_SetThreadName(const char *name)
inform the debugger of the current thread&#39;s name.
Definition: bdbg.cpp:126
void SDL_DestroySemaphore(SDL_sem *sem)
Definition: wsdl.cpp:1420
char m_ErrorBuffer[CURL_ERROR_SIZE]
Definition: UserReport.cpp:500
#define ENSURE(expr)
ensure the expression &lt;expr&gt; evaluates to non-zero.
Definition: debug.h:282
#define PROFILE2(region)
Starts timing from now until the end of the current scope.
Definition: Profiler2.h:446
int pthread_create(pthread_t *thread_id, const void *attr, void *(*func)(void *), void *arg)
Definition: wpthread.cpp:636
void SubmitReport(const char *type, int version, const std::string &data)
Submit a report to be transmitted to the online server.
Definition: UserReport.cpp:616
CProfiler2 g_Profiler2
Definition: Profiler2.cpp:35
static const int REPORTER_VERSION
Version number stored in config file when the user agrees to the reporting.
Definition: UserReport.cpp:59
bool Shutdown()
Called by main thread to request shutdown.
Definition: UserReport.cpp:174
New profiler (complementing the older CProfileManager)
void RegisterCurrentThread(const std::string &name)
Call in any thread to enable the profiler in that thread.
Definition: Profiler2.cpp:241
void Submit(const shared_ptr< CUserReport > &report)
Called by main thread to add a new report to the queue.
Definition: UserReport.cpp:203
#define SAFE_DELETE(p)
delete memory ensuing from new and set the pointer to zero (thus making double-frees safe / a no-op) ...
void Update()
Called by the main thread every frame, so we can check retransmission timers.
Definition: UserReport.cpp:218
#define g_ConfigDB
Definition: ConfigDB.h:52
void ConstructRequestData(const CUserReport &report)
Definition: UserReport.cpp:399
double timer_Time()
Definition: timer.cpp:98
Status sys_generate_random_bytes(u8 *buf, size_t count)
generate high-quality random bytes.
Definition: unix.cpp:318
static void * RunThread(void *data)
Definition: UserReport.cpp:231
std::string LoadUserID()
Definition: UserReport.cpp:518
#define TIMER(description)
Measures the time taken to execute code up until end of the current scope; displays it via debug_prin...
Definition: timer.h:108
std::wstring wstring_from_utf8(const std::string &src, Status *err)
convert UTF-8 to a wide string (UTF-16 or UCS-4, depending on the platform&#39;s wchar_t).
Definition: utf8.cpp:225
SDL_sem * SDL_CreateSemaphore(int cnt)
Definition: wsdl.cpp:1414
void RecordRegionEnter(const char *id)
Definition: Profiler2.h:315
uintptr_t pthread_t
Definition: wpthread.h:63
void SetReportingEnabled(bool enabled)
Definition: UserReport.cpp:554
std::string m_UserID
Definition: UserReport.cpp:491
static size_t SendCallback(char *bufptr, size_t size, size_t nmemb, void *userp)
Definition: UserReport.cpp:451
std::string GetStatus()
Called by main thread to determine the current status of the uploader.
Definition: UserReport.cpp:194
time_t m_Time
Definition: UserReport.cpp:77
std::string m_Type
Definition: UserReport.cpp:78
static size_t ReceiveCallback(void *buffer, size_t size, size_t nmemb, void *userp)
Definition: UserReport.cpp:439
void RecordRegionLeave(const char *id)
Definition: Profiler2.h:320
int pthread_join(pthread_t thread, void **value_ptr)
Definition: wpthread.cpp:679
void SetEnabled(bool enabled)
Called by main thread, when the online reporting is enabled/disabled.
Definition: UserReport.cpp:155
std::string m_Status
Definition: UserReport.cpp:487
int SDL_SemPost(SDL_sem *sem)
Definition: wsdl.cpp:1426
int SDL_SemWait(SDL_sem *sem)
Definition: wsdl.cpp:1432
void Initialize()
Definition: UserReport.cpp:573
void debug_printf(const wchar_t *fmt,...)
write a formatted string to the debug channel, subject to filtering (see below).
Definition: debug.cpp:142
bool IsReportingEnabled()
Definition: UserReport.cpp:547
void Deinitialize()
Definition: UserReport.cpp:590
std::string m_Data
Definition: UserReport.cpp:80