-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtrace_client.hpp
More file actions
1166 lines (1084 loc) · 43.6 KB
/
Copy pathtrace_client.hpp
File metadata and controls
1166 lines (1084 loc) · 43.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* trace-client 0.3.0 (C++) -- https://github.com/Stephenson-Software/trace-client-cpp
*
* One call to report that a program was used. Copy this header into a project
* as is; there is nothing else to add. C++11 or later, no library to link
* beyond the threads library (-pthread), as for any std::thread.
*
* MIT licensed. Keep this header when vendoring so the file can be found again.
*/
#ifndef TRACE_CLIENT_HPP
#define TRACE_CLIENT_HPP
#define TRACE_CLIENT_VERSION "0.3.0"
// The executable that carries a report over HTTPS: the system's own curl
// (shipped with macOS, with Windows 10 1803 and later, and with nearly every
// Linux distribution). Override before including to point somewhere else.
// Define TRACE_CLIENT_USE_LIBCURL instead to link against libcurl.
#ifndef TRACE_CLIENT_CURL
#define TRACE_CLIENT_CURL "curl"
#endif
#include <cctype>
#include <cerrno>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <fstream>
#include <functional>
#include <locale>
#include <map>
#include <memory>
#include <mutex>
#include <random>
#include <sstream>
#include <string>
#include <thread>
#include <sys/types.h>
#include <sys/stat.h>
#if defined(_WIN32)
#include <direct.h>
#endif
#if defined(TRACE_CLIENT_USE_LIBCURL)
#include <curl/curl.h>
#elif defined(__EMSCRIPTEN__)
// No processes and no threads worth the name in a browser: the client is
// always disabled there (reason "unavailable").
#elif defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#define TRACE_CLIENT_UNDEF_LEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#define TRACE_CLIENT_UNDEF_NOMINMAX
#endif
#include <windows.h>
#ifdef TRACE_CLIENT_UNDEF_LEAN
#undef WIN32_LEAN_AND_MEAN
#undef TRACE_CLIENT_UNDEF_LEAN
#endif
#ifdef TRACE_CLIENT_UNDEF_NOMINMAX
#undef NOMINMAX
#undef TRACE_CLIENT_UNDEF_NOMINMAX
#endif
#else
#include <cerrno>
#include <fcntl.h>
#include <signal.h>
#include <spawn.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#if defined(__APPLE__)
#include <crt_externs.h>
#else
extern char **environ;
#endif
#define TRACE_CLIENT_POSIX 1
#endif
namespace trace_client {
/** String tags on a report, such as {{"name", "home"}}. */
typedef std::map<std::string, std::string> Tags;
/**
* Where dropped reports are mentioned. Called on the client's own thread for
* failed deliveries and on the reporting thread for a full queue.
*/
typedef std::function<void(const std::string &)> Logger;
/** How many reports may wait to be sent before new ones are dropped. */
static const std::size_t QUEUE_CAPACITY = 256;
/** Per-request timeout, and the most close() waits for queued reports. */
static const double TIMEOUT_SECONDS = 5.0;
/** The server's limits: tags per report, and length of a name or tag key/value. */
static const std::size_t MAX_TAGS = 32;
static const std::size_t MAX_LENGTH = 255;
/** Environment variables that turn reporting off for every trace-reporting program. */
static const char *const ENV_TRACE_USAGE_REPORTING = "TRACE_USAGE_REPORTING";
static const char *const ENV_DO_NOT_TRACK = "DO_NOT_TRACK";
/** The values disabledReason() can take. First match wins, in this order. */
static const char *const REASON_ENVIRONMENT = "environment";
static const char *const REASON_CONFIG = "config";
static const char *const REASON_NO_KEY = "no key";
/** No way to send on this platform, or the client could not start its thread. */
static const char *const REASON_UNAVAILABLE = "unavailable";
namespace detail {
inline std::string getEnvironment(const char *name) {
#if defined(_MSC_VER)
char *value = NULL;
std::size_t length = 0;
std::string result;
if (_dupenv_s(&value, &length, name) == 0 && value != NULL) {
result = value;
}
std::free(value);
return result;
#else
const char *value = std::getenv(name);
return value == NULL ? std::string() : std::string(value);
#endif
}
inline std::string trimLower(const std::string &text) {
std::size_t begin = 0, end = text.size();
while (begin < end && std::isspace(static_cast<unsigned char>(text[begin]))) ++begin;
while (end > begin && std::isspace(static_cast<unsigned char>(text[end - 1]))) --end;
std::string out = text.substr(begin, end - begin);
for (std::size_t i = 0; i < out.size(); ++i) {
out[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(out[i])));
}
return out;
}
inline bool isBlank(const std::string &text) {
for (std::size_t i = 0; i < text.size(); ++i) {
if (!std::isspace(static_cast<unsigned char>(text[i]))) return false;
}
return true;
}
inline std::string trim(const std::string &text) {
std::size_t begin = 0, end = text.size();
while (begin < end && std::isspace(static_cast<unsigned char>(text[begin]))) ++begin;
while (end > begin && std::isspace(static_cast<unsigned char>(text[end - 1]))) --end;
return text.substr(begin, end - begin);
}
/**
* The event's own tags plus "version", unless the event already carries one.
* A copy; the caller's tags are never modified.
*/
inline Tags withVersion(const Tags &tags, const std::string &version) {
Tags merged(tags);
merged.insert(Tags::value_type("version", version)); // never overwrites
return merged;
}
/**
* Valid UTF-8, at most maxBytes long: invalid sequences become U+FFFD and a
* code point is never cut in half. The server stores what it is sent, and a
* string it cannot decode would cost the whole report.
*/
inline std::string cleanUtf8(const std::string &text, std::size_t maxBytes) {
static const char replacement[] = "\xEF\xBF\xBD";
std::string out;
out.reserve(text.size() < maxBytes ? text.size() : maxBytes);
std::size_t i = 0;
const std::size_t n = text.size();
while (i < n) {
unsigned char c = static_cast<unsigned char>(text[i]);
std::size_t length = 0;
unsigned long cp = 0;
if (c < 0x80) { length = 1; cp = c; }
else if (c >= 0xC2 && c <= 0xDF) { length = 2; cp = c & 0x1F; }
else if (c >= 0xE0 && c <= 0xEF) { length = 3; cp = c & 0x0F; }
else if (c >= 0xF0 && c <= 0xF4) { length = 4; cp = c & 0x07; }
bool valid = length > 0 && i + length <= n;
for (std::size_t k = 1; valid && k < length; ++k) {
unsigned char next = static_cast<unsigned char>(text[i + k]);
if ((next & 0xC0) != 0x80) valid = false;
else cp = (cp << 6) | (next & 0x3F);
}
if (valid && ((length == 3 && (cp < 0x800 || (cp >= 0xD800 && cp <= 0xDFFF)))
|| (length == 4 && (cp < 0x10000 || cp > 0x10FFFF)))) {
valid = false;
}
if (valid) {
if (out.size() + length > maxBytes) break;
out.append(text, i, length);
i += length;
} else {
if (out.size() + 3 > maxBytes) break;
out.append(replacement, 3);
i += 1;
}
}
return out;
}
/** A JSON string literal. Input is expected to be valid UTF-8 already. */
inline std::string quote(const std::string &text) {
std::string out;
out.reserve(text.size() + 2);
out += '"';
for (std::size_t i = 0; i < text.size(); ++i) {
unsigned char c = static_cast<unsigned char>(text[i]);
switch (c) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
case '\b': out += "\\b"; break;
case '\f': out += "\\f"; break;
default:
if (c < 0x20) {
static const char hex[] = "0123456789abcdef";
out += "\\u00";
out += hex[c >> 4];
out += hex[c & 0x0F];
} else {
out += static_cast<char>(c);
}
}
}
out += '"';
return out;
}
/** The shortest decimal that reads back as value, whatever the C locale. */
inline std::string number(double value) {
std::string text;
for (int precision = 1; precision <= 17; ++precision) {
std::ostringstream out;
out.imbue(std::locale::classic());
out.precision(precision);
out << value;
text = out.str();
std::istringstream in(text);
in.imbue(std::locale::classic());
double back = 0;
in >> back;
if (back == value) break;
}
return text;
}
/**
* The report body. JSON is written by hand so this file has no dependencies;
* the shape is fixed and small, three scalars and a flat string map.
*/
/** The tag every event carries the installation's ID as. */
static const char *const INSTALL_TAG = "install";
/**
* The tags plus "install", unless they already carry one, there is no ID, or
* adding it would pass MAX_TAGS. A copy; the caller's tags are never modified.
*/
inline Tags withInstall(const Tags &tags, const std::string &installId) {
if (installId.empty() || tags.count(INSTALL_TAG) != 0 || tags.size() >= MAX_TAGS) return tags;
Tags merged(tags);
merged.insert(Tags::value_type(INSTALL_TAG, installId));
return merged;
}
/** Whether text is a usable installation ID: 1..MAX_LENGTH of [A-Za-z0-9_.-]. */
inline bool isValidInstallId(const std::string &text) {
if (text.empty() || text.size() > MAX_LENGTH) return false;
for (std::size_t i = 0; i < text.size(); ++i) {
unsigned char c = static_cast<unsigned char>(text[i]);
bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
|| c == '_' || c == '.' || c == '-';
if (!ok) return false;
}
return true;
}
/**
* A random (version 4) UUID, lower-case 8-4-4-4-12 hex. Derived from nothing
* but std::random_device; falls back to the clock if that is unavailable.
*/
inline std::string randomUuid() {
unsigned long long hi = 0, lo = 0;
try {
std::random_device device;
std::seed_seq seed{device(), device(), device(), device(), device(), device(), device(), device()};
std::mt19937_64 engine(seed);
hi = engine();
lo = engine();
} catch (...) {
std::mt19937_64 engine(static_cast<unsigned long long>(
std::chrono::high_resolution_clock::now().time_since_epoch().count()));
hi = engine();
lo = engine();
}
hi = (hi & ~0xF000ULL) | 0x4000ULL; // version 4
lo = (lo & 0x3FFFFFFFFFFFFFFFULL) | 0x8000000000000000ULL; // RFC 4122 variant
char out[37];
std::snprintf(out, sizeof out, "%08llx-%04llx-%04llx-%04llx-%012llx",
(hi >> 32) & 0xFFFFFFFFULL, (hi >> 16) & 0xFFFFULL, hi & 0xFFFFULL,
(lo >> 48) & 0xFFFFULL, lo & 0xFFFFFFFFFFFFULL);
return std::string(out);
}
enum PathKind { PATH_MISSING, PATH_FILE, PATH_OTHER };
/** Missing (may be created), a regular file, or anything else (left alone). */
inline PathKind pathKind(const std::string &path) {
#if defined(_WIN32)
struct _stat info;
if (_stat(path.c_str(), &info) != 0) {
return errno == ENOENT || errno == ENOTDIR ? PATH_MISSING : PATH_OTHER;
}
return (info.st_mode & _S_IFMT) == _S_IFREG ? PATH_FILE : PATH_OTHER;
#else
struct stat info;
if (::stat(path.c_str(), &info) != 0) {
return errno == ENOENT || errno == ENOTDIR ? PATH_MISSING : PATH_OTHER;
}
return S_ISREG(info.st_mode) ? PATH_FILE : PATH_OTHER;
#endif
}
/** Creates every missing directory above path. Failures are ignored: the write that follows says. */
inline void makeParentDirectories(const std::string &path) {
for (std::size_t i = 1; i < path.size(); ++i) {
char c = path[i];
#if defined(_WIN32)
if (c != '/' && c != '\\') continue;
if (path[i - 1] == ':') continue; // a drive, "C:\\"
_mkdir(path.substr(0, i).c_str());
#else
if (c != '/') continue;
::mkdir(path.substr(0, i).c_str(), 0777);
#endif
}
}
inline std::string json(const std::string &application, const std::string &name,
bool hasValue, double value, const Tags &tags) {
std::string out = "{\"application\":" + quote(cleanUtf8(application, std::string::npos));
out += ",\"name\":" + quote(cleanUtf8(name, MAX_LENGTH));
if (hasValue && !std::isnan(value) && !std::isinf(value)) {
out += ",\"value\":" + number(value);
}
std::string pairs;
std::size_t count = 0;
for (Tags::const_iterator tag = tags.begin(); tag != tags.end() && count < MAX_TAGS; ++tag) {
std::string key = cleanUtf8(tag->first, MAX_LENGTH);
if (isBlank(key)) continue;
if (count > 0) pairs += ',';
pairs += quote(key) + ':' + quote(cleanUtf8(tag->second, MAX_LENGTH));
++count;
}
if (count > 0) out += ",\"tags\":{" + pairs + '}';
out += '}';
return out;
}
/** Drops control characters: nothing typed into a header may end the line. */
inline std::string headerSafe(const std::string &text) {
std::string out;
for (std::size_t i = 0; i < text.size(); ++i) {
unsigned char c = static_cast<unsigned char>(text[i]);
if (c >= 0x20 && c != 0x7F) out += static_cast<char>(c);
}
return out;
}
/**
* A quoted value in curl's config-file syntax. curl unescapes \\ \" \t \n \r
* \v inside quotes; every backslash and quote is escaped and every control
* character is written as an escape, so a value can never end its line and
* start another option.
*/
inline std::string curlQuote(const std::string &text) {
std::string out = "\"";
for (std::size_t i = 0; i < text.size(); ++i) {
char c = text[i];
switch (c) {
case '\\': out += "\\\\"; break;
case '"': out += "\\\""; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
case '\v': out += "\\v"; break;
default:
if (static_cast<unsigned char>(c) >= 0x20) out += c;
}
}
out += '"';
return out;
}
struct Shared {
std::string endpoint;
std::string key;
std::string application;
Logger logger;
std::mutex mutex;
std::condition_variable changed;
std::deque<std::string> queue;
bool closing; // close() was called: send what is queued, then stop
bool abandoned; // close() gave up waiting: stop now, drop the rest
bool done; // the sending thread has stopped
#if defined(TRACE_CLIENT_POSIX)
pid_t child; // the curl process in flight, or 0
#elif defined(_WIN32) && !defined(TRACE_CLIENT_USE_LIBCURL)
HANDLE child; // the curl process in flight, or NULL
#endif
Shared() : closing(false), abandoned(false), done(false)
#if defined(TRACE_CLIENT_POSIX)
, child(0)
#elif defined(_WIN32) && !defined(TRACE_CLIENT_USE_LIBCURL)
, child(NULL)
#endif
{}
// Guards logger alone, so a logger may itself call report(). close()
// clears the logger under it: once close() returns, the program's logger
// is never called again, even by a thread that close() gave up on.
std::mutex logMutex;
void log(const std::string &message) {
try {
std::lock_guard<std::mutex> lock(logMutex);
if (logger) logger("[trace] " + message);
} catch (...) {
// a logger that throws must not stop the sending thread
}
}
void forgetLogger() {
try {
std::lock_guard<std::mutex> lock(logMutex);
logger = Logger();
} catch (...) {
}
}
/** Called by close() with the mutex held, once it has given up waiting. */
void killChild() {
#if defined(TRACE_CLIENT_POSIX)
if (child > 0) ::kill(child, SIGKILL);
#elif defined(_WIN32) && !defined(TRACE_CLIENT_USE_LIBCURL)
if (child != NULL) ::TerminateProcess(child, 1);
#endif
}
};
inline long timeoutWholeSeconds() {
return static_cast<long>(TIMEOUT_SECONDS);
}
#if !defined(TRACE_CLIENT_USE_LIBCURL) && !defined(__EMSCRIPTEN__)
/** Everything curl is told, on its standard input rather than its command line. */
inline std::string curlConfig(const Shared &shared, const std::string &body) {
std::ostringstream timeout;
timeout.imbue(std::locale::classic());
timeout << timeoutWholeSeconds();
std::string config;
config += "url = " + curlQuote(shared.endpoint) + "\n";
config += "proto = \"=http,https\"\n";
config += "header = \"Content-Type: application/json; charset=utf-8\"\n";
config += "header = \"Expect:\"\n"; // no 100-continue round trip for a larger body
config += "header = " + curlQuote("Authorization: Bearer " + headerSafe(shared.key)) + "\n";
config += "user-agent = " + curlQuote("trace-client-cpp/" TRACE_CLIENT_VERSION " (" + headerSafe(shared.application) + ")") + "\n";
config += "data-binary = " + curlQuote(body) + "\n";
config += "max-time = " + timeout.str() + "\n";
config += "connect-timeout = " + timeout.str() + "\n";
#if defined(_WIN32)
config += "output = \"NUL\"\n";
#else
config += "output = \"/dev/null\"\n";
#endif
config += "write-out = \"%{http_code}\"\n";
return config;
}
inline int parseStatus(const std::string &out) {
int status = 0;
bool any = false;
for (std::size_t i = 0; i < out.size(); ++i) {
if (out[i] >= '0' && out[i] <= '9') {
status = status * 10 + (out[i] - '0');
any = true;
if (status > 999) return -1;
} else if (!std::isspace(static_cast<unsigned char>(out[i]))) {
return -1;
}
}
return any && status > 0 ? status : -1;
}
#endif
#if defined(TRACE_CLIENT_USE_LIBCURL)
inline std::size_t discardBody(char *, std::size_t size, std::size_t count, void *) {
return size * count;
}
#if LIBCURL_VERSION_NUM >= 0x072000
inline int abortWhenAbandoned(void *data, curl_off_t, curl_off_t, curl_off_t, curl_off_t) {
Shared *shared = static_cast<Shared *>(data);
std::lock_guard<std::mutex> lock(shared->mutex);
return shared->abandoned ? 1 : 0;
}
#endif
/** Returns the HTTP status, or -1 with error set. */
inline int send(Shared &shared, const std::string &body, std::string &error) {
CURL *curl = curl_easy_init();
if (curl == NULL) {
error = "curl_easy_init failed";
return -1;
}
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json; charset=utf-8");
headers = curl_slist_append(headers, "Expect:");
headers = curl_slist_append(headers, ("Authorization: Bearer " + headerSafe(shared.key)).c_str());
std::string agent = "trace-client-cpp/" TRACE_CLIENT_VERSION " (" + headerSafe(shared.application) + ")";
curl_easy_setopt(curl, CURLOPT_URL, shared.endpoint.c_str());
#if LIBCURL_VERSION_NUM >= 0x075500
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "http,https");
#else
curl_easy_setopt(curl, CURLOPT_PROTOCOLS, static_cast<long>(CURLPROTO_HTTP | CURLPROTO_HTTPS));
#endif
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_USERAGENT, agent.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(body.size()));
curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeoutWholeSeconds());
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, timeoutWholeSeconds());
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, discardBody);
#if LIBCURL_VERSION_NUM >= 0x072000
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, abortWhenAbandoned);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &shared);
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
#endif
CURLcode result = curl_easy_perform(curl);
long status = -1;
if (result == CURLE_OK) {
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
} else {
error = curl_easy_strerror(result);
}
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return static_cast<int>(status);
}
#elif defined(TRACE_CLIENT_POSIX)
inline void closeQuietly(int fd) {
if (fd >= 0) ::close(fd);
}
inline bool closeOnExec(int fd) {
int flags = ::fcntl(fd, F_GETFD);
return flags != -1 && ::fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1;
}
/**
* Runs `curl -q --silent --config -` with the request on its standard input.
* The argument vector is fixed -- nothing from the program or the report is
* ever an argument, and no shell is involved. Returns the HTTP status, or -1
* with error set (curl missing, unreachable server, timeout).
*/
inline int send(Shared &shared, const std::string &body, std::string &error) {
int in[2] = {-1, -1};
int out[2] = {-1, -1};
if (::pipe(in) != 0 || ::pipe(out) != 0 || !closeOnExec(in[0]) || !closeOnExec(in[1])
|| !closeOnExec(out[0]) || !closeOnExec(out[1])) {
error = "could not create a pipe";
closeQuietly(in[0]); closeQuietly(in[1]); closeQuietly(out[0]); closeQuietly(out[1]);
return -1;
}
#if defined(F_SETNOSIGPIPE)
::fcntl(in[1], F_SETNOSIGPIPE, 1);
#endif
posix_spawn_file_actions_t actions;
posix_spawnattr_t attributes;
posix_spawn_file_actions_init(&actions);
posix_spawnattr_init(&attributes);
posix_spawn_file_actions_adddup2(&actions, in[0], 0);
posix_spawn_file_actions_adddup2(&actions, out[1], 1);
posix_spawn_file_actions_addopen(&actions, 2, "/dev/null", O_WRONLY, 0);
// curl starts with an ordinary signal mask and SIGPIPE at its default,
// whatever the sending thread (which blocks SIGPIPE) or the host set.
sigset_t none, pipe;
sigemptyset(&none);
sigemptyset(&pipe);
sigaddset(&pipe, SIGPIPE);
posix_spawnattr_setsigmask(&attributes, &none);
posix_spawnattr_setsigdefault(&attributes, &pipe);
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF);
char program[] = TRACE_CLIENT_CURL;
char arg0[] = "curl";
char arg1[] = "-q"; // first, so no ~/.curlrc is read
char arg2[] = "--silent";
char arg3[] = "--config";
char arg4[] = "-";
char *argv[] = {arg0, arg1, arg2, arg3, arg4, NULL};
#if defined(__APPLE__)
char **env = *_NSGetEnviron();
#else
char **env = environ;
#endif
pid_t pid = 0;
int spawned = posix_spawnp(&pid, program, &actions, &attributes, argv, env);
posix_spawn_file_actions_destroy(&actions);
posix_spawnattr_destroy(&attributes);
closeQuietly(in[0]);
closeQuietly(out[1]);
if (spawned != 0) {
closeQuietly(in[1]);
closeQuietly(out[0]);
error = std::string("could not run " TRACE_CLIENT_CURL ": ") + std::strerror(spawned);
return -1;
}
{
std::lock_guard<std::mutex> lock(shared.mutex);
shared.child = pid;
if (shared.abandoned) shared.killChild();
}
std::string config = curlConfig(shared, body);
std::size_t written = 0;
while (written < config.size()) {
ssize_t n = ::write(in[1], config.data() + written, config.size() - written);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) break; // EPIPE: curl is gone; its exit status says why
written += static_cast<std::size_t>(n);
}
closeQuietly(in[1]);
std::string answer;
char buffer[64];
for (;;) {
ssize_t n = ::read(out[0], buffer, sizeof buffer);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) break;
if (answer.size() < 64) answer.append(buffer, static_cast<std::size_t>(n));
}
closeQuietly(out[0]);
// Wait without reaping first, so close() can never signal a pid that has
// already been reused: the zombie holds the pid until waitpid below.
siginfo_t info;
std::memset(&info, 0, sizeof info);
while (::waitid(P_PID, static_cast<id_t>(pid), &info, WEXITED | WNOWAIT) != 0 && errno == EINTR) {
}
{
std::lock_guard<std::mutex> lock(shared.mutex);
shared.child = 0;
}
int status = 0;
bool exited = false;
for (;;) {
pid_t reaped = ::waitpid(pid, &status, 0);
if (reaped == pid) { exited = true; break; }
if (reaped < 0 && errno == EINTR) continue;
break; // ECHILD: the host reaps its own children; judge by the output
}
int http = parseStatus(answer);
if (http > 0) return http;
if (exited && WIFEXITED(status) && WEXITSTATUS(status) == 127) {
error = "could not run " TRACE_CLIENT_CURL;
} else if (exited && WIFEXITED(status)) {
std::ostringstream message;
message << "curl exited with " << WEXITSTATUS(status);
error = message.str();
} else {
error = "curl did not finish";
}
return -1;
}
#elif defined(_WIN32)
inline bool findCurl(std::string &path) {
char buffer[MAX_PATH];
UINT length = ::GetSystemDirectoryA(buffer, MAX_PATH);
if (length > 0 && length < MAX_PATH) {
std::string candidate = std::string(buffer, length) + "\\curl.exe";
DWORD attributes = ::GetFileAttributesA(candidate.c_str());
if (attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY)) {
path = candidate;
return true;
}
}
// Only the directories on PATH -- never the current directory, where a
// stray curl.exe next to the program would otherwise be picked up.
std::string searchPath = getEnvironment("PATH");
if (searchPath.empty()) return false;
std::string name = std::string(TRACE_CLIENT_CURL) + ".exe";
DWORD found = ::SearchPathA(searchPath.c_str(), name.c_str(), NULL, MAX_PATH, buffer, NULL);
if (found == 0 || found >= MAX_PATH) return false;
path.assign(buffer, found);
return true;
}
/** See the POSIX send(): same fixed command line, request on standard input. */
inline int send(Shared &shared, const std::string &body, std::string &error) {
std::string curl;
if (!findCurl(curl)) {
error = "could not find curl.exe";
return -1;
}
SECURITY_ATTRIBUTES inherit;
inherit.nLength = sizeof inherit;
inherit.lpSecurityDescriptor = NULL;
inherit.bInheritHandle = TRUE;
HANDLE inRead = NULL, inWrite = NULL, outRead = NULL, outWrite = NULL, nul = INVALID_HANDLE_VALUE;
bool ready = ::CreatePipe(&inRead, &inWrite, &inherit, 1 << 16)
&& ::SetHandleInformation(inWrite, HANDLE_FLAG_INHERIT, 0)
&& ::CreatePipe(&outRead, &outWrite, &inherit, 0)
&& ::SetHandleInformation(outRead, HANDLE_FLAG_INHERIT, 0);
if (ready) {
nul = ::CreateFileA("NUL", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &inherit,
OPEN_EXISTING, 0, NULL);
ready = nul != INVALID_HANDLE_VALUE;
}
PROCESS_INFORMATION process;
std::memset(&process, 0, sizeof process);
if (ready) {
STARTUPINFOA startup;
std::memset(&startup, 0, sizeof startup);
startup.cb = sizeof startup;
startup.dwFlags = STARTF_USESTDHANDLES;
startup.hStdInput = inRead;
startup.hStdOutput = outWrite;
startup.hStdError = nul;
char commandLine[] = "curl.exe -q --silent --config -";
ready = ::CreateProcessA(curl.c_str(), commandLine, NULL, NULL, TRUE, CREATE_NO_WINDOW,
NULL, NULL, &startup, &process) != 0;
if (!ready) error = "could not run curl.exe";
} else {
error = "could not create a pipe";
}
if (inRead) ::CloseHandle(inRead);
if (outWrite) ::CloseHandle(outWrite);
if (nul != INVALID_HANDLE_VALUE) ::CloseHandle(nul);
if (!ready) {
if (inWrite) ::CloseHandle(inWrite);
if (outRead) ::CloseHandle(outRead);
return -1;
}
::CloseHandle(process.hThread);
{
std::lock_guard<std::mutex> lock(shared.mutex);
shared.child = process.hProcess;
if (shared.abandoned) shared.killChild();
}
std::string config = curlConfig(shared, body);
std::size_t written = 0;
while (written < config.size()) {
DWORD n = 0;
if (!::WriteFile(inWrite, config.data() + written, static_cast<DWORD>(config.size() - written), &n, NULL) || n == 0) break;
written += n;
}
::CloseHandle(inWrite);
std::string answer;
char buffer[64];
for (;;) {
DWORD n = 0;
if (!::ReadFile(outRead, buffer, sizeof buffer, &n, NULL) || n == 0) break;
if (answer.size() < 64) answer.append(buffer, n);
}
::CloseHandle(outRead);
::WaitForSingleObject(process.hProcess, INFINITE);
DWORD exitCode = 0;
::GetExitCodeProcess(process.hProcess, &exitCode);
{
std::lock_guard<std::mutex> lock(shared.mutex);
shared.child = NULL;
}
::CloseHandle(process.hProcess);
int http = parseStatus(answer);
if (http > 0) return http;
std::ostringstream message;
message << "curl exited with " << exitCode;
error = message.str();
return -1;
}
#endif
#if !defined(__EMSCRIPTEN__) || defined(TRACE_CLIENT_USE_LIBCURL)
#define TRACE_CLIENT_CAN_SEND 1
inline void deliver(Shared &shared, const std::string &body) {
std::string error;
int status = -1;
try {
status = send(shared, body, error);
} catch (...) {
error = "unexpected failure";
}
if (status < 0) {
shared.log("could not deliver " + body + ": " + error);
} else if (status != 201) {
std::ostringstream message;
message << "trace server answered " << status << " for " << body;
shared.log(message.str());
}
}
inline void drain(std::shared_ptr<Shared> shared) {
#if defined(TRACE_CLIENT_POSIX)
// A write to a curl that died early must be an EPIPE here, never a
// SIGPIPE that kills the host program. Only this thread is affected.
sigset_t pipe;
sigemptyset(&pipe);
sigaddset(&pipe, SIGPIPE);
pthread_sigmask(SIG_BLOCK, &pipe, NULL);
#endif
for (;;) {
std::string body;
{
std::unique_lock<std::mutex> lock(shared->mutex);
while (shared->queue.empty() && !shared->closing && !shared->abandoned) {
shared->changed.wait(lock);
}
if (shared->abandoned || shared->queue.empty()) {
shared->done = true;
shared->queue.clear();
shared->changed.notify_all();
return;
}
body.swap(shared->queue.front());
shared->queue.pop_front();
}
deliver(*shared, body);
}
}
#endif
} // namespace detail
/**
* Whether the environment asks for usage reporting to be off, via
* TRACE_USAGE_REPORTING=off (also false, 0, no) or DO_NOT_TRACK=1 (also true,
* yes), case-insensitive. Any other value, or none, leaves the program's own
* setting in charge.
*/
inline bool environmentOptsOut() {
std::string reporting = detail::trimLower(detail::getEnvironment(ENV_TRACE_USAGE_REPORTING));
if (reporting == "off" || reporting == "false" || reporting == "0" || reporting == "no") return true;
std::string dnt = detail::trimLower(detail::getEnvironment(ENV_DO_NOT_TRACK));
return dnt == "1" || dnt == "true" || dnt == "yes";
}
/**
* Where an installation's ID comes from, for the constructor that takes one.
* An explicit id wins over a file; with neither, no "install" tag is sent.
*/
struct InstallId {
/** An ID the program stores itself. Trimmed; blank means none. */
std::string id;
/** A file installIdFromFile keeps a random UUID in. Blank means none. */
std::string file;
static InstallId of(const std::string &id) {
InstallId install;
install.id = id;
return install;
}
static InstallId fromFile(const std::string &path) {
InstallId install;
install.file = path;
return install;
}
};
/**
* Reports usage events to a trace server, and never gets in the way of the
* program doing the reporting.
*
* - report() returns immediately: the HTTP call happens on one thread the
* client owns, so a game loop can report without a frame waiting.
* - Nothing here throws. A server that is down, slow or rejecting the key,
* or a machine without curl, is a dropped report, mentioned to the
* optional Logger and otherwise not at all.
* - At most QUEUE_CAPACITY reports wait to be sent; later ones are dropped.
* - close() (also run by the destructor) gives queued reports up to
* TIMEOUT_SECONDS in total, then stops -- a CLI that reports and exits at
* once still sends, and an unreachable server never hangs the exit.
*
* Reporting is opt-out. The constructor decides once, first match wins, and
* disabledReason() says which: the environment (REASON_ENVIRONMENT), the
* program's own setting (REASON_CONFIG), a blank key (REASON_NO_KEY), or no
* way to send (REASON_UNAVAILABLE).
*
* Every event carries the program's own version as the tag "version" -- the
* third argument to the constructor, required, so a "command" event can be
* tied to a release as well as a "startup" one. An event's own "version" tag
* wins over it. It is trimmed; a blank one, or one longer than MAX_LENGTH
* bytes, is treated like a blank base URL or application: nothing throws, the
* client reports nothing and disabledReason() is REASON_UNAVAILABLE.
*
* Every event can also carry a random per-installation ID as the tag
* "install", so the trace server can count installations rather than raw
* events. There is no hidden default: without an InstallId argument no
* "install" tag is sent. InstallId::fromFile(path) keeps a random UUID in a
* file the program chooses (see installIdFromFile); InstallId::of(id) passes
* one the program stores itself. Either is resolved only once every opt-out
* above has been checked, so a disabled client never makes up an ID or writes
* one. An event's own "install" tag wins, and it is never added past MAX_TAGS.
*
* trace_client::TraceClient trace("https://trace.example.org", "MyGame", MYGAME_VERSION,
* settings.key,
* trace_client::InstallId::fromFile(dataDir + "/trace-install-id"),
* settings.usageReporting);
* trace.report("startup");
* ...
* trace.close(); // or let the destructor do it
*/
class TraceClient {
public:
/** A client that reports nothing (reason "config"). */
TraceClient() : version_("disabled"), reason_(REASON_CONFIG) {}
/**
* A client for version (the program's own, sent as the tag "version" on
* every event) of application, reporting to the trace server at baseUrl.
* Its events carry no "install" tag; see the overload taking an InstallId.
*/
TraceClient(const std::string &baseUrl, const std::string &application, const std::string &version,
const std::string &key, bool enabled = true, Logger logger = Logger()) {
init(baseUrl, application, version, key, InstallId(), enabled, logger);
}
/**
* As above, and every event carries the installation's ID (install) as
* the tag "install". The explicit ID is trimmed; a blank one means none,
* and one longer than MAX_LENGTH bytes is treated like an overlong
* version: nothing throws, the client reports nothing and
* disabledReason() is REASON_UNAVAILABLE.
*/
TraceClient(const std::string &baseUrl, const std::string &application, const std::string &version,
const std::string &key, const InstallId &install, bool enabled = true, Logger logger = Logger()) {
init(baseUrl, application, version, key, install, enabled, logger);
}
/**
* The installation's ID kept in the file at path: the first line that is
* 1..MAX_LENGTH characters of [A-Za-z0-9_.-] (surrounding whitespace
* ignored). When the file is missing, or has no such line, a new random
* UUID is written to it (parent directories created) and returned. When
* the path exists but cannot be read, or the write fails, a new random
* UUID is returned for this process only and nothing is written. Never
* throws.
*
* Calling this directly writes the file whatever the opt-outs say; pass
* InstallId::fromFile(path) to the constructor instead so that a disabled
* client never writes it.
*/
static std::string installIdFromFile(const std::string &path) {
std::string fresh;
try {
fresh = detail::randomUuid();
detail::PathKind kind = detail::pathKind(path);
if (path.empty() || kind == detail::PATH_OTHER) return fresh;
if (kind == detail::PATH_FILE) {
std::ifstream in(path.c_str(), std::ios::in | std::ios::binary);
if (!in) return fresh; // there but unreadable: never overwritten
std::string line;
while (std::getline(in, line)) {
std::string id = detail::trim(line);
if (detail::isValidInstallId(id)) return id;
}
if (in.bad()) return fresh;
}
detail::makeParentDirectories(path);
std::ofstream out(path.c_str(), std::ios::out | std::ios::trunc | std::ios::binary);
if (out) {
out << fresh << '\n';
out.flush();
}
} catch (...) {
// an ID that only lives in memory is still an ID