Skip to content

Commit

Permalink
[Telemetry] Detect if vcpkg is running in a container (#730)
Browse files Browse the repository at this point in the history
* [wip] Implement Windows Container heuristics

* Add Linux containers heuristics

* Use GetUserNameW() on Windows

* Add detected container metric

* Reorder metrics

* Test cgroup parser

* Apply suggestions from code review

Co-authored-by: Billy O'Neal <[email protected]>

* Update src/vcpkg.cpp

* Fix osx build by removing unnecessary std::moves.

Co-authored-by: Billy O'Neal <[email protected]>
  • Loading branch information
vicroms and BillyONeal authored Oct 12, 2022
1 parent d775ad2 commit b586c27
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ node_modules/
/ce/test/vcpkg-ce.test.build.log
/ce/common/temp
/vcpkg-root
/CMakePresets.json
4 changes: 4 additions & 0 deletions include/vcpkg/base/system.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ namespace vcpkg
const ExpectedS<Path>& get_system_root() noexcept;

const ExpectedS<Path>& get_system32() noexcept;

std::wstring get_username();

bool test_registry_key(void* base_hkey, StringView sub_key);
#endif

Optional<std::string> get_registry_string(void* base_hkey, StringView subkey, StringView valuename);
Expand Down
21 changes: 21 additions & 0 deletions include/vcpkg/cgroup-parser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include <vcpkg/base/fwd/stringview.h>

#include <string>

namespace vcpkg
{
struct ControlGroup
{
long hierarchy_id;
std::string subsystems;
std::string control_group;

ControlGroup(long id, StringView s, StringView c);
};

std::vector<ControlGroup> parse_cgroup_file(StringView text, StringView origin);

bool detect_docker_in_cgroup_file(StringView text, StringView origin);
}
1 change: 1 addition & 0 deletions include/vcpkg/metrics.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ namespace vcpkg

enum class BoolMetric
{
DetectedContainer,
InstallManifestMode,
OptionOverlayPorts,
COUNT // always keep COUNT last
Expand Down
66 changes: 66 additions & 0 deletions src/vcpkg-test/cgroup-parser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#include <catch2/catch.hpp>

#include <vcpkg/base/stringview.h>

#include <vcpkg/cgroup-parser.h>

using namespace vcpkg;

TEST_CASE ("parse", "[cgroup-parser]")
{
auto ok_text = R"(
3:cpu:/
2:cpuset:/
1:memory:/
0::/
)";

auto cgroups = parse_cgroup_file(ok_text, "ok_text");
REQUIRE(cgroups.size() == 4);
CHECK(cgroups[0].hierarchy_id == 3);
CHECK(cgroups[0].subsystems == "cpu");
CHECK(cgroups[0].control_group == "/");
CHECK(cgroups[1].hierarchy_id == 2);
CHECK(cgroups[1].subsystems == "cpuset");
CHECK(cgroups[1].control_group == "/");
CHECK(cgroups[2].hierarchy_id == 1);
CHECK(cgroups[2].subsystems == "memory");
CHECK(cgroups[2].control_group == "/");
CHECK(cgroups[3].hierarchy_id == 0);
CHECK(cgroups[3].subsystems == "");
CHECK(cgroups[3].control_group == "/");

auto cgroups_short = parse_cgroup_file("2::", "short_text");
REQUIRE(cgroups_short.size() == 1);
CHECK(cgroups_short[0].hierarchy_id == 2);
CHECK(cgroups_short[0].subsystems == "");
CHECK(cgroups_short[0].control_group == "");

auto cgroups_incomplete = parse_cgroup_file("0:/", "incomplete_text");
CHECK(cgroups_incomplete.empty());

auto cgroups_bad_id = parse_cgroup_file("ab::", "non_numeric_id_text");
CHECK(cgroups_bad_id.empty());

auto cgroups_empty = parse_cgroup_file("", "empty");
CHECK(cgroups_empty.empty());
}

TEST_CASE ("detect docker", "[cgroup-parser]")
{
auto with_docker = R"(
2:memory:/docker/66a5f8000f3f2e2a19c3f7d60d870064d26996bdfe77e40df7e3fc955b811d14
1:name=systemd:/docker/66a5f8000f3f2e2a19c3f7d60d870064d26996bdfe77e40df7e3fc955b811d14
0::/docker/66a5f8000f3f2e2a19c3f7d60d870064d26996bdfe77e40df7e3fc955b811d14
)";

auto without_docker = R"(
3:cpu:/
2:cpuset:/
1:memory:/
0::/
)";

CHECK(detect_docker_in_cgroup_file(with_docker, "with_docker"));
CHECK(!detect_docker_in_cgroup_file(without_docker, "without_docker"));
}
50 changes: 50 additions & 0 deletions src/vcpkg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <vcpkg/base/system.debug.h>
#include <vcpkg/base/system.process.h>

#include <vcpkg/cgroup-parser.h>
#include <vcpkg/commands.contact.h>
#include <vcpkg/commands.h>
#include <vcpkg/commands.version.h>
Expand Down Expand Up @@ -41,6 +42,47 @@ static void invalid_command(const std::string& cmd)
Checks::exit_fail(VCPKG_LINE_INFO);
}

static bool detect_container(vcpkg::Filesystem& fs)
{
(void)fs;
#if defined(_WIN32)
if (test_registry_key(HKEY_LOCAL_MACHINE, R"(SYSTEM\CurrentControlSet\Services\cexecsvc)"))
{
Debug::println("Detected Container Execution Service");
return true;
}

auto username = get_username();
if (username == L"ContainerUser" || username == L"ContainerAdministrator")
{
Debug::println("Detected container username");
return true;
}
#elif defined(__linux__)
if (fs.exists("/.dockerenv", IgnoreErrors{}))
{
Debug::println("Detected /.dockerenv file");
return true;
}

// check /proc/1/cgroup, if we're running in a container then the control group for each hierarchy will be:
// /docker/<containerid>, or
// /lxc/<containerid>
//
// Example of /proc/1/cgroup contents:
// 2:memory:/docker/66a5f8000f3f2e2a19c3f7d60d870064d26996bdfe77e40df7e3fc955b811d14
// 1:name=systemd:/docker/66a5f8000f3f2e2a19c3f7d60d870064d26996bdfe77e40df7e3fc955b811d14
// 0::/docker/66a5f8000f3f2e2a19c3f7d60d870064d26996bdfe77e40df7e3fc955b811d14
auto cgroup_contents = fs.read_contents("/proc/1/cgroup", IgnoreErrors{});
if (detect_docker_in_cgroup_file(cgroup_contents, "/proc/1/cgroup"))
{
Debug::println("Detected docker in cgroup");
return true;
}
#endif
return false;
}

static void inner(vcpkg::Filesystem& fs, const VcpkgCmdArguments& args)
{
// track version on each invocation
Expand Down Expand Up @@ -68,6 +110,14 @@ static void inner(vcpkg::Filesystem& fs, const VcpkgCmdArguments& args)
}
};

{
auto metrics = LockGuardPtr<Metrics>(g_metrics);
if (metrics->metrics_enabled())
{
metrics->track_bool_property(BoolMetric::DetectedContainer, detect_container(fs));
}
}

LockGuardPtr<Metrics>(g_metrics)->track_bool_property(BoolMetric::OptionOverlayPorts, !args.overlay_ports.empty());

if (const auto command_function = find_command(Commands::get_available_basic_commands()))
Expand Down
20 changes: 20 additions & 0 deletions src/vcpkg/base/system.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#endif

#if defined(_WIN32)
#include <lmcons.h>
#include <winbase.h>
// needed for mingw
#include <processenv.h>
#else
Expand Down Expand Up @@ -346,6 +348,24 @@ namespace vcpkg
return hkey_type == REG_SZ || hkey_type == REG_MULTI_SZ || hkey_type == REG_EXPAND_SZ;
}

std::wstring get_username()
{
DWORD buffer_size = UNLEN + 1;
std::wstring buffer;
buffer.resize(static_cast<size_t>(buffer_size));
GetUserNameW(buffer.data(), &buffer_size);
buffer.resize(buffer_size);
return buffer;
}

bool test_registry_key(void* base_hkey, StringView sub_key)
{
HKEY k = nullptr;
const LSTATUS ec =
RegOpenKeyExW(reinterpret_cast<HKEY>(base_hkey), Strings::to_utf16(sub_key).c_str(), 0, KEY_READ, &k);
return (ERROR_SUCCESS == ec);
}

Optional<std::string> get_registry_string(void* base_hkey, StringView sub_key, StringView valuename)
{
HKEY k = nullptr;
Expand Down
70 changes: 70 additions & 0 deletions src/vcpkg/cgroup-parser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#include <vcpkg/base/parse.h>
#include <vcpkg/base/stringview.h>
#include <vcpkg/base/system.debug.h>
#include <vcpkg/base/util.h>

#include <vcpkg/cgroup-parser.h>

namespace vcpkg
{
ControlGroup::ControlGroup(long id, StringView s, StringView c)
: hierarchy_id(id), subsystems(s.data(), s.size()), control_group(c.data(), c.size())
{
}

// parses /proc/[pid]/cgroup file as specified in https://linux.die.net/man/5/proc
// The file describes control groups to which the process/tasks belongs.
// For each cgroup hierarchy there is one entry
// containing colon-separated fields of the form:
// 5:cpuacct,cpu,cpuset:/daemos
//
// The colon separated fields are, from left to right:
//
// 1. hierarchy ID number
// 2. set of subsystems bound to the hierarchy
// 3. control group in the hierarchy to which the process belongs
std::vector<ControlGroup> parse_cgroup_file(StringView text, StringView origin)
{
using P = ParserBase;
constexpr auto is_separator_or_lineend = [](auto ch) { return ch == ':' || P::is_lineend(ch); };

auto parser = ParserBase(text, origin);
parser.skip_whitespace();

std::vector<ControlGroup> ret;
while (!parser.at_eof())
{
auto id = parser.match_until(is_separator_or_lineend);
auto maybe_numeric_id = Strings::strto<long>(id);
if (!maybe_numeric_id || P::is_lineend(parser.cur()))
{
ret.clear();
break;
}

parser.next();
auto subsystems = parser.match_until(is_separator_or_lineend);
if (P::is_lineend(parser.cur()))
{
ret.clear();
break;
}

parser.next();
auto control_group = parser.match_until(P::is_lineend);
parser.skip_whitespace();

ret.emplace_back(*maybe_numeric_id.get(), subsystems, control_group);
}

return ret;
}

bool detect_docker_in_cgroup_file(StringView text, StringView origin)
{
return Util::any_of(parse_cgroup_file(text, origin), [](auto&& cgroup) {
return Strings::starts_with(cgroup.control_group, "/docker") ||
Strings::starts_with(cgroup.control_group, "/lxc");
});
}
}
1 change: 1 addition & 0 deletions src/vcpkg/metrics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ namespace vcpkg
}};

const constexpr std::array<BoolMetricEntry, static_cast<size_t>(BoolMetric::COUNT)> all_bool_metrics{{
{BoolMetric::DetectedContainer, "detected_container"},
{BoolMetric::InstallManifestMode, "install_manifest_mode"},
{BoolMetric::OptionOverlayPorts, "option_overlay_ports"},
}};
Expand Down

0 comments on commit b586c27

Please sign in to comment.