Skip to content

Commit

Permalink
Implement update notifications and light telemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
timschumi committed Oct 14, 2023
1 parent d0cdec6 commit 51c9905
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .github/workflows/commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,7 @@ jobs:
run: |
"${GITHUB_WORKSPACE}/gmod-chttp/tests/server.py" 1>/dev/null 2>&1 &
until curl --silent "http://127.0.0.1:5000" 1>/dev/null 2>&1; do sleep 1; done
export CHTTP_DISABLE_UPDATE_NOTIFICATION=1
export CHTTP_DISABLE_TELEMETRY=1
./runner test.lua
kill $!
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.10)
project(gmod-chttp LANGUAGES CXX)

set(CHTTP_VERSION "dev" CACHE STRING "The user-visible CHTTP version")
set(CHTTP_BUILD_TARGET "unset" CACHE STRING "")
set(CHTTP_BUILD_TYPE "unset" CACHE STRING "")
set(CHTTP_BUILD_STATIC "unset" CACHE STRING "")

set(CMAKE_CONFIGURATION_TYPES Release Debug)
set(CMAKE_CXX_STANDARD 11)
Expand Down Expand Up @@ -30,6 +33,9 @@ if(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
endif()

target_compile_definitions(chttp PRIVATE "CHTTP_VERSION=\"${CHTTP_VERSION}\"")
target_compile_definitions(chttp PRIVATE "CHTTP_BUILD_TARGET=\"${CHTTP_BUILD_TARGET}\"")
target_compile_definitions(chttp PRIVATE "CHTTP_BUILD_TYPE=\"${CHTTP_BUILD_TYPE}\"")
target_compile_definitions(chttp PRIVATE "CHTTP_BUILD_STATIC=\"${CHTTP_BUILD_STATIC}\"")

set_gmod_suffix_prefix(chttp)

Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,37 @@ requests while in singleplayer or while the server is hibernating.
In case there are any issues (bug reports greatly appreciated), you can return to
using the old hooking method by setting the environment variable `CHTTP_FORCE_HOOK`.

### Update notifications and telemetry

CHTTP automatically checks for new updates and transmits light telemetry data
when the module is loaded. This behavior can be disabled by setting the
`CHTTP_DISABLE_UPDATE_NOTIFICATION` and `CHTTP_DISABLE_TELEMETRY` environment
variables respectively.

Unless both features are disabled (in which case no request is sent at all),
the current module version is always transmitted (for the purpose of checking
if any newer versions are available).

If telemetry isn't disabled, the following additional properties are sent as well:

* `build_target`: `win32`, `win64`, `linux` or `linux64`
* `build_type`: `Release` or `Debug`
* `build_static` `0` or `1` (depending on whether the `-static` file is in use)

If telemetry isn't disabled and the host OS is Linux, the following additional
properties are sent:

* `os_sysname`: `Linux`
* `os_release`: Linux kernel version (e.g. `6.5.7-arch1-1` on Arch Linux)
* `os_version`: Linux kernel build string (e.g. `#1 SMP PREEMPT_DYNAMIC Tue, 10 Oct 2023 21:10:21 +0000`)
* `os_machine`: Processor architecture (e.g. `x86_64`)
* `dist_name`: Distribution name (e.g. `ubuntu`)
* `dist_version`: Distribution version (if present; e.g. `22.04`)

I'm collecting and aggregating this data for my own curiosity, as well as getting
an informed guess on how fast updates propagate and what systems the module is run on.
If you have any feedback regarding either feature, feel free to leave it [here](https://github.com/timschumi/gmod-chttp/discussions/29).

## Addon development

This is only required for developers who want to use CHTTP in their addons.
Expand Down
3 changes: 3 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ cmake \
-DCMAKE_TOOLCHAIN_FILE=toolchain-${BUILD_TARGET}.cmake \
-DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
-DCHTTP_VERSION=${CHTTP_VERSION} \
-DCHTTP_BUILD_TARGET=${BUILD_TARGET} \
-DCHTTP_BUILD_TYPE=${BUILD_TYPE} \
-DCHTTP_BUILD_STATIC=${build_static:-0} \
"${cmake_args[@]}" \
"${BASE_DIR}"
make
115 changes: 115 additions & 0 deletions src/chttp.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
#include <algorithm>
#include <curl/curl.h>
#include <fstream>
#include <sstream>
#include <string>
#ifdef __linux__
# include <sys/utsname.h>
#endif

#include "GarrysMod/Lua/Interface.h"

Expand Down Expand Up @@ -135,6 +141,113 @@ LUA_FUNCTION(CHTTP)
return 1; // We are returning a single value
}

LUA_FUNCTION(handle_update_response)
{
double code = LUA->GetNumber(1);

// Both code 204 (no update) and all other codes (server down?) will be ignored silently.
if (code != 200)
return 0;

std::map<std::string, std::string> headers;
lua_table_to_map(LUA, 3, headers);

// Treat responses without the magic header as malformed.
std::string version_header_name = "Chttp-Update-Message-Version";
if (headers.count(version_header_name) == 0) {
version_header_name = "chttp-update-message-version";
if (headers.count(version_header_name) == 0)
return 0;
}

auto version = std::stoi(headers[version_header_name]);
if (version < 1)
return 0;

if (version > 1) {
Logger::warn("The update server responded with a newer-than-supported message version.");
Logger::warn("Please manually check the GitHub page for new updates:");
Logger::warn("https://github.com/timschumi/gmod-chttp/releases");
return 0;
}

unsigned int raw_body_length;
char const* raw_body = LUA->GetString(2, &raw_body_length);
std::string body(raw_body, raw_body_length);

std::istringstream body_stream(body);
std::string line;

while (std::getline(body_stream, line)) {
Logger::warn("%s", line.c_str());
}

return 0;
}

void handle_updates_or_telemetry(GarrysMod::Lua::ILuaBase* LUA)
{
bool disable_update_notification = getenv("CHTTP_DISABLE_UPDATE_NOTIFICATION");
bool disable_telemetry = getenv("CHTTP_DISABLE_TELEMETRY");

// No need to do anything if we don't want update information and don't want to check in.
if (disable_update_notification && disable_telemetry)
return;

auto request = std::make_shared<HTTPRequest>();
request->method = HTTPMethod::Post;
request->url = "https://chttp.timschumi.net/checkin";
request->timeout = 5;

if (!disable_update_notification) {
LUA->PushCFunction(handle_update_response);
request->success = std::make_shared<LuaReference>(LUA);
}

request->parameters["version"] = CHTTP_VERSION;
if (!disable_telemetry) {
request->parameters["build_target"] = CHTTP_BUILD_TARGET;
request->parameters["build_type"] = CHTTP_BUILD_TYPE;
request->parameters["build_static"] = CHTTP_BUILD_STATIC;

#ifdef __linux__
struct utsname utsname { };
if (uname(&utsname) >= 0) {
// As much as I'd like to for deduplication purposes, the nodename (i.e. hostname) is not sent.
request->parameters["os_sysname"] = utsname.sysname;
request->parameters["os_release"] = utsname.release;
request->parameters["os_version"] = utsname.version;
request->parameters["os_machine"] = utsname.machine;
}

std::ifstream os_release_file("/etc/os-release");
std::string os_release_line;

while (std::getline(os_release_file, os_release_line)) {
std::string key;
std::string value;

if (os_release_line.rfind("ID=", 0) == 0) {
key = "dist_name";
value = os_release_line.substr(3);
} else if (os_release_line.rfind("VERSION_ID=", 0) == 0) {
key = "dist_version";
value = os_release_line.substr(11);
} else {
continue;
}

// Strip quotes from the value.
value.erase(std::remove(value.begin(), value.end(), '"'), value.end());

request->parameters[key] = value;
}
#endif
}

RequestWorker::the().requests().push(request);
}

GMOD_MODULE_OPEN()
{
// Set up logging
Expand Down Expand Up @@ -180,6 +293,8 @@ GMOD_MODULE_OPEN()
// Start the background thread
RequestWorker::the();

handle_updates_or_telemetry(LUA);

return 0;
}

Expand Down
2 changes: 1 addition & 1 deletion src/lua.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ void lua_table_to_map(GarrysMod::Lua::ILuaBase* LUA, int index, std::map<std::st
// ->Next() gets the last key from the stack and pushes
// the key-value pair that follows that to the stack.
// key will now be top-2 and value will be top-1
while (LUA->Next(index - 1) != 0) {
while (LUA->Next(index < 0 ? index - 1 : index) != 0) {
// Remove entries with non-string keys
if (!LUA->IsType(-2, GarrysMod::Lua::Type::String)) {
Logger::devwarn("Ignoring non-string key in table!");
Expand Down

0 comments on commit 51c9905

Please sign in to comment.