Program Listing for File Telemetry.cpp
↰ Return to documentation for file (src/flamegpu/io/Telemetry.cpp
)
#include "flamegpu/io/Telemetry.h"
#include <cstdlib>
#include <cerrno>
#include <algorithm>
#include <memory>
#include <array>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <cstdio>
#include <cctype>
#include <sstream>
#include <string>
#include <map>
#include <random>
#ifdef _WIN32
#include <windows.h>
#include <shlobj.h>
#else
#include <sys/types.h>
#include <unistd.h>
#include <pwd.h>
#endif
#include "flamegpu/version.h"
namespace flamegpu {
namespace io {
namespace {
// the FLAMEGPU2 telemetry app id.
const char TELEMETRY_APP_ID[] = "94AC5E3F-F674-4E29-BF87-DAF4BA7F8F79";
// Flag tracking if telemetry is enabled, initialised subject to preproc and env vars in initialiseFromEnvironmentIfNeeded
static bool enabled = false;
// Flag indicating if users have suppressed telemetry warnings
static bool suppressed = false;
// Flag indicating if FLAMEGPU telemetry test mode is enabled.
static bool testMode = false;
// Flag indicating if these anon namespace statics have been enabled or not
static bool initialised = false;
// Flag indicating if the user has been notified / encouraged to enable usage statistics or not
static bool haveNotified = false;
bool cmakeStrToBool(const char * input) {
// Assume truthy
bool rtn = true;
if (input != NULL) {
std::string s = std::string(input);
// Trim leading whitespace
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
// Trim trailing whitespace
s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
// Transform the input to lower case
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return static_cast<unsigned char>(std::tolower(c)); });
// If it's a falsey option, set it to falesy.
if (s == "0" || s == "false" || s == "off") {
rtn = false;
}
}
return rtn;
}
/*
* Initialise namespace scoped static variables based from environment variables, if not done already.
This is done in a method to avoid potential UB with initialisation order of static members and the system environment, which maybe undefined based on SO posts / is not just a direct mapping.
*/
void initialiseFromEnvironmentIfNeeded() {
if (!initialised) {
// Enabled by default
enabled = true;
// If the sharing environment var is set, parse it following cmake boolean logic
char * env_FLAMEGPU_SHARE_USAGE_STATISTICS = std::getenv("FLAMEGPU_SHARE_USAGE_STATISTICS");
if (env_FLAMEGPU_SHARE_USAGE_STATISTICS != NULL) {
enabled = cmakeStrToBool(env_FLAMEGPU_SHARE_USAGE_STATISTICS);
} else {
// if the environment variable is not specified, use the value from the preprocessor
#ifdef FLAMEGPU_SHARE_USAGE_STATISTICS
enabled = true;
#else
enabled = false;
#endif
}
// Parse env and cmake variables to find the default value for suppression.
suppressed = false;
char * env_FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE = std::getenv("FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE");
if (env_FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE != NULL) {
suppressed = cmakeStrToBool(env_FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE);
} else {
// if the environment variable is not specified, use the value from the preprocessor
#ifdef FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE
suppressed = true;
#else
suppressed = false;
#endif
}
// Parse env and cmake variables to find the default value for test dev mode. .
testMode = false;
char * env_FLAMEGPU_TELEMETRY_TEST_MODE = std::getenv("FLAMEGPU_TELEMETRY_TEST_MODE");
if (env_FLAMEGPU_TELEMETRY_TEST_MODE != NULL) {
testMode = cmakeStrToBool(env_FLAMEGPU_TELEMETRY_TEST_MODE);
} else {
// if the environment variable is not specified, use the value from the preprocessor
#ifdef FLAMEGPU_TELEMETRY_TEST_MODE
testMode = true;
#else
testMode = false;
#endif
}
// Mark this as initialised.
initialised = true;
}
}
} // anonymous namespace
void Telemetry::enable() {
// Initialise from the env var is needed. A ctor might be nicer.
initialiseFromEnvironmentIfNeeded();
// set to enabled.
enabled = true;
}
void Telemetry::disable() {
// initialise from the env var if needed
initialiseFromEnvironmentIfNeeded();
// set to disabled.
enabled = false;
}
bool Telemetry::isEnabled() {
// initialise from the env var if needed
initialiseFromEnvironmentIfNeeded();
// return if it is enabled or not.
return enabled;
}
void Telemetry::suppressNotice() {
// initialise from the env var if needed
initialiseFromEnvironmentIfNeeded();
// set the suppressed flag in the anon namespace
suppressed = true;
}
bool Telemetry::isTestMode() {
// initialise from the env var if needed
initialiseFromEnvironmentIfNeeded();
// return if it is enabled or not.
return testMode;
}
std::string Telemetry::generateData(std::string event_name, std::map<std::string, std::string> payload_items, bool isSWIG) {
// Initialise from the env var is needed. A ctor might be nicer.
initialiseFromEnvironmentIfNeeded();
const std::string var_testmode = "$TEST_MODE";
const std::string var_appID = "$APP_ID";
const std::string var_telemetryRandomID = "$TELEMETRY_RANDOM_ID";
const std::string var_eventName = "$EVENT_TYPE";
const std::string var_payload = "$PAYLOAD";
// check ENV for test variable FLAMEGPU_TEST_ENVIRONMENT
std::string testmode = isTestMode() ? "true" : "false";
std::string appID = TELEMETRY_APP_ID;
std::string telemetryRandomID = Telemetry::getUserId(); // Obtain a unique user ID
// Differentiate pyflamegpu in the payload via the SWIG compiler macro, which we only define when building for pyflamegpu.
// A user could potentially static link against a build using that macro, but that's not a use-case we are currently concerned with.
if (isSWIG) {
std::string py_version = "pyflamegpu" + std::string(flamegpu::VERSION_STRING);
payload_items["appVersion"] = py_version; // e.g. 'pyflamegpu2.0.0-alpha.3' (graphed in Telemetry deck)
} else {
payload_items["appVersion"] = flamegpu::VERSION_STRING; // e.g. '2.0.0-alpha.3' (graphed in Telemetry deck)
}
// other version strings
payload_items["appVersionFull"] = flamegpu::VERSION_FULL;
payload_items["majorSystemVersion"] = std::to_string(flamegpu::VERSION_MAJOR); // e.g. '2' (graphed in Telemetry deck)
std::string major_minor_patch = std::to_string(flamegpu::VERSION_MAJOR) + "." + std::to_string(flamegpu::VERSION_MINOR) + "." + std::to_string(flamegpu::VERSION_PATCH);
payload_items["majorMinorSystemVersion"] = major_minor_patch; // e.g. '2.0.0' (graphed in Telemetry deck)
payload_items["appVersionPatch"] = std::to_string(flamegpu::VERSION_PATCH);
payload_items["appVersionPreRelease"] = flamegpu::VERSION_PRERELEASE;
payload_items["buildNumber"] = flamegpu::VERSION_BUILDMETADATA; // e.g. '0553592f' (graphed in Telemetry deck)
payload_items["buildID"] = flamegpu::TELEMETRY_RANDOM_ID; // e.g. 'e7e0fe30325c83a3ad52e2cb2180e50979d3e6bcb0692789977a06ad09889843' (ID generated a cmake time)
// OS
#ifdef _WIN32
payload_items["operatingSystem"] = "Windows";
#elif __linux__
payload_items["operatingSystem"] = "Linux";
#elif __unix__
payload_items["operatingSystem"] = "Unix";
#else
payload_items["operatingSystem"] = "Other";
#endif
// visualiastion status
#ifdef FLAMEGPU_VISUALISATION
payload_items["Visualisation"] = "true";
#else
payload_items["Visualisation"] = "false";
#endif
// create the payload
std::string payload = "";
bool first = true;
// iterate payload to generate the payload array
for (const auto& [key, value] : payload_items) {
std::string item_str;
if (!first)
item_str = ",\"" + key + ":" + value + "\"";
else
item_str = "\"" + key + ":" + value + "\"";
payload.append(item_str);
first = false;
}
// create the telemetry json package
std::string telemetry_data = R"json(
[{
"isTestMode": "$TEST_MODE",
"appID": "$APP_ID",
"clientUser": "$TELEMETRY_RANDOM_ID",
"sessionID": "",
"type" : "$EVENT_TYPE",
"payload" : [$PAYLOAD]
}])json";
// update the placeholders
telemetry_data.replace(telemetry_data.find(var_testmode), var_testmode.length(), testmode);
telemetry_data.replace(telemetry_data.find(var_appID), var_appID.length(), appID);
telemetry_data.replace(telemetry_data.find(var_telemetryRandomID), var_telemetryRandomID.length(), telemetryRandomID);
telemetry_data.replace(telemetry_data.find(var_eventName), var_eventName.length(), event_name);
telemetry_data.replace(telemetry_data.find(var_payload), var_payload.length(), payload);
// Remove newlines and replace with space
telemetry_data.erase(std::remove(telemetry_data.begin(), telemetry_data.end(), '\n'), telemetry_data.end());
// Remove tabs and replace with space
telemetry_data.erase(std::remove(telemetry_data.begin(), telemetry_data.end(), '\t'), telemetry_data.end());
// Remove spaces
telemetry_data.erase(std::remove_if(telemetry_data.begin(), telemetry_data.end(), [](char c) {
return std::isspace(static_cast<unsigned char>(c));
}), telemetry_data.end());
// Use escape characters
size_t pos = 0;
while ((pos = telemetry_data.find("\"", pos)) != std::string::npos) {
telemetry_data.replace(pos, 1, "\\\"");
pos += 2;
}
return telemetry_data;
}
bool Telemetry::sendData(std::string telemetry_data) {
// Initialise from the env var is needed. A ctor might be nicer.
initialiseFromEnvironmentIfNeeded();
// Maximum duration curl to attempt to connect to the endpoint
const float CURL_CONNECT_TIMEOUT = 0.5;
// Maximum total duration for the curl call, including connection and payload
const float CURL_MAX_TIME = 1.0;
// Silent curl command (-s) and redirect response output to null
std::string null;
#if _WIN32
null = "nul";
#else
null = "/dev/null";
#endif
std::stringstream curl_command;
curl_command << "curl";
curl_command << " -s";
curl_command << " -o " << null;
curl_command << " --connect-timeout " << std::to_string(CURL_CONNECT_TIMEOUT);
curl_command << " --max-time " << std::to_string(CURL_MAX_TIME);
curl_command << " -X POST \"" << std::string(TELEMETRY_ENDPOINT) << "\"";
curl_command << " -H \"Content-Type: application/json; charset=utf-8\"";
curl_command << " --data-raw \"" << telemetry_data + "\"";
curl_command << " > " << null << " 2>&1";
// capture the return value
if (std::system(curl_command.str().c_str()) != EXIT_SUCCESS) {
return false;
}
return true;
}
void Telemetry::encourageUsage() {
// Only print the usage encouragement notice if it has not already been encouraged, telemetry is not enabled and telemetry has not been suppressed.
if (!haveNotified && !suppressed && !isEnabled()) {
fprintf(stdout,
"NOTICE: The FLAME GPU software is reliant on evidence to support is continued development. Please "
"consider enabling FLAMEGPU_SHARE_USAGE_STATISTICS during CMake configuration, "
"setting FLAMEGPU_SHARE_USAGE_STATISTICS to true as an environment variable, "
"or setting the Simulation/Ensemble config telemetry property to true.\n"
"This message can be silenced by suppressing all output (--quiet), "
"calling flamegpu::io::Telemetry::suppressNotice, or defining a system environment variable FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE\n");
// Set the flag that this has already been emitted once during execution of the current binary file, so it doesn't happen again.
haveNotified = true;
}
}
std::string Telemetry::getConfigDirectory() {
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
return std::string(path);
}
throw std::runtime_error("Unable to retrieve config directory on Windows");
#else
const char* configHome = std::getenv("XDG_CONFIG_HOME");
if (configHome) {
return std::string(configHome);
}
const char* home = std::getenv("HOME");
if (home) {
return std::string(home) + "/.config";
}
// try and get the user directory if home is not set
struct passwd pwd;
struct passwd* result = nullptr;
char buffer[4096];
int ret = getpwuid_r(getuid(), &pwd, buffer, sizeof(buffer), &result);
if (ret == 0 && result != nullptr) {
return std::string(pwd.pw_dir) + "/.config";
}
throw std::runtime_error("Unable to retrieve config directory on Linux");
#endif
}
std::string Telemetry::generateRandomId() {
const char charset[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
const size_t length = 36;
std::random_device rd;
std::mt19937 generator(rd());
std::uniform_int_distribution<size_t> distribution(0, sizeof(charset) - 2);
std::string randomId;
for (size_t i = 0; i < length; ++i) {
randomId += charset[distribution(generator)];
}
return randomId;
}
std::string Telemetry::getUserId() {
// Generate and a new random 36-character user ID
std::string userId = Telemetry::generateRandomId();
// Try to load an existing ID from file if it exists
try {
// Determine config file location
std::string configDir = Telemetry::getConfigDirectory() + "/flamegpu";
std::filesystem::create_directories(configDir); // Ensure the directory exists
std::string filePath = configDir + "/telemetry_user.cfg";
// Check if the file exists
if (std::filesystem::exists(filePath)) {
std::ifstream file(filePath, std::ios_base::binary);
if (file.is_open()) {
std::string cached_id;
std::getline(file, cached_id); // overwrite existing Id
file.close();
// Config file and user id found so return it if not empty (either because file is externally modified or file is a directory)
if (!cached_id.empty())
return cached_id;
else
return userId;
}
else {
throw std::runtime_error("Unable to open user ID file for reading");
}
}
std::ofstream file(filePath, std::ios_base::binary);
if (file.is_open()) {
file << userId;
file.close();
}
else {
throw std::runtime_error("Unable to create user ID file");
}
} catch (const std::exception&) {
fprintf(stderr, "Warning: Telemetry User Id file is not read/writeable from config file. A new User Id will be used.\n");
}
return userId;
}
} // namespace io
} // namespace flamegpu