From 71f8958a069208b08a4bcca207956ef8a15238ab Mon Sep 17 00:00:00 2001 From: Robert Schumacher Date: Fri, 1 Dec 2017 16:08:09 -0800 Subject: [PATCH] [vcpkg-contact-survey] Add monthly survey prompt --- toolsrc/include/pch.h | 1 + toolsrc/include/tests.pch.h | 1 + toolsrc/include/vcpkg/base/chrono.h | 19 +++++ toolsrc/include/vcpkg/globalstate.h | 2 + toolsrc/include/vcpkg/userconfig.h | 20 +++++ toolsrc/src/tests.chrono.cpp | 41 +++++++++ toolsrc/src/vcpkg.cpp | 95 +++++++++------------ toolsrc/src/vcpkg/base/chrono.cpp | 62 ++++++++++++++ toolsrc/src/vcpkg/commands.contact.cpp | 13 +++ toolsrc/src/vcpkg/globalstate.cpp | 2 + toolsrc/src/vcpkg/metrics.cpp | 28 ++---- toolsrc/src/vcpkg/userconfig.cpp | 83 ++++++++++++++++++ toolsrc/vcpkglib/vcpkglib.vcxproj | 2 + toolsrc/vcpkglib/vcpkglib.vcxproj.filters | 6 ++ toolsrc/vcpkgtest/vcpkgtest.vcxproj | 1 + toolsrc/vcpkgtest/vcpkgtest.vcxproj.filters | 3 + 16 files changed, 301 insertions(+), 78 deletions(-) create mode 100644 toolsrc/include/vcpkg/userconfig.h create mode 100644 toolsrc/src/tests.chrono.cpp create mode 100644 toolsrc/src/vcpkg/userconfig.cpp diff --git a/toolsrc/include/pch.h b/toolsrc/include/pch.h index 5c31fbbd1d6..683bef171b8 100644 --- a/toolsrc/include/pch.h +++ b/toolsrc/include/pch.h @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include diff --git a/toolsrc/include/tests.pch.h b/toolsrc/include/tests.pch.h index 0037af585dc..5c00fca4aa0 100644 --- a/toolsrc/include/tests.pch.h +++ b/toolsrc/include/tests.pch.h @@ -2,6 +2,7 @@ #include +#include #include #include #include diff --git a/toolsrc/include/vcpkg/base/chrono.h b/toolsrc/include/vcpkg/base/chrono.h index c791f53fa1c..4291115f72a 100644 --- a/toolsrc/include/vcpkg/base/chrono.h +++ b/toolsrc/include/vcpkg/base/chrono.h @@ -2,6 +2,8 @@ #include #include +#include +#include namespace vcpkg::Chrono { @@ -44,4 +46,21 @@ namespace vcpkg::Chrono private: std::chrono::high_resolution_clock::time_point m_start_tick; }; + + class CTime + { + public: + static Optional get_current_date_time(); + static Optional parse(CStringView str); + + constexpr CTime() : m_tm{0} {} + explicit constexpr CTime(tm t) : m_tm{t} {} + + std::string to_string() const; + + std::chrono::system_clock::time_point to_time_point() const; + + private: + mutable tm m_tm; + }; } diff --git a/toolsrc/include/vcpkg/globalstate.h b/toolsrc/include/vcpkg/globalstate.h index 40ec7958e88..360d3f43e07 100644 --- a/toolsrc/include/vcpkg/globalstate.h +++ b/toolsrc/include/vcpkg/globalstate.h @@ -10,6 +10,8 @@ namespace vcpkg struct GlobalState { static Util::LockGuarded timer; + static Util::LockGuarded g_surveydate; + static std::atomic debugging; static std::atomic feature_packages; diff --git a/toolsrc/include/vcpkg/userconfig.h b/toolsrc/include/vcpkg/userconfig.h new file mode 100644 index 00000000000..63b8e54818f --- /dev/null +++ b/toolsrc/include/vcpkg/userconfig.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace vcpkg +{ + struct UserConfig + { + std::string user_id; + std::string user_time; + std::string user_mac; + + std::string last_completed_survey; + + static UserConfig try_read_data(const Files::Filesystem& fs); + + void try_write_data(Files::Filesystem& fs) const; + }; +} diff --git a/toolsrc/src/tests.chrono.cpp b/toolsrc/src/tests.chrono.cpp new file mode 100644 index 00000000000..269cdca5804 --- /dev/null +++ b/toolsrc/src/tests.chrono.cpp @@ -0,0 +1,41 @@ +#include "tests.pch.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace Chrono = vcpkg::Chrono; + +namespace UnitTest1 +{ + class ChronoTests : public TestClass + { + TEST_METHOD(parse_time) + { + auto timestring = "1990-02-03T04:05:06.0Z"; + auto maybe_time = Chrono::CTime::parse(timestring); + + Assert::IsTrue(maybe_time.has_value()); + + Assert::AreEqual(timestring, maybe_time.get()->to_string().c_str()); + } + + TEST_METHOD(parse_time_blank) + { + auto maybe_time = Chrono::CTime::parse(""); + + Assert::IsFalse(maybe_time.has_value()); + } + + TEST_METHOD(time_difference) + { + auto maybe_time1 = Chrono::CTime::parse("1990-02-03T04:05:06.0Z"); + auto maybe_time2 = Chrono::CTime::parse("1990-02-10T04:05:06.0Z"); + + Assert::IsTrue(maybe_time1.has_value()); + Assert::IsTrue(maybe_time2.has_value()); + + auto delta = maybe_time2.get()->to_time_point() - maybe_time1.get()->to_time_point(); + + Assert::AreEqual(24 * 7, std::chrono::duration_cast(delta).count()); + } + }; +} diff --git a/toolsrc/src/vcpkg.cpp b/toolsrc/src/vcpkg.cpp index 04d44c414ba..094ea1dc5d1 100644 --- a/toolsrc/src/vcpkg.cpp +++ b/toolsrc/src/vcpkg.cpp @@ -20,11 +20,13 @@ #include #include #include +#include #include #include #include #include +#include #pragma comment(lib, "ole32") #pragma comment(lib, "shell32") @@ -110,6 +112,28 @@ static void inner(const VcpkgCmdArguments& args) if (args.command != "autocomplete") { Commands::Version::warn_if_vcpkg_version_mismatch(paths); + std::string surveydate = *GlobalState::g_surveydate.lock(); + auto maybe_surveydate = Chrono::CTime::parse(surveydate); + if (auto p_surveydate = maybe_surveydate.get()) + { + auto delta = std::chrono::system_clock::now() - p_surveydate->to_time_point(); + // 24 hours/day * 30 days/month + if (std::chrono::duration_cast(delta).count() > 24 * 30) + { + std::default_random_engine generator( + static_cast(std::chrono::system_clock::now().time_since_epoch().count())); + std::uniform_int_distribution distribution(1, 4); + + if (distribution(generator) == 1) + { + Metrics::g_metrics.lock()->track_property("surveyprompt", "true"); + System::println( + System::Color::success, + "Your feedback is important to improve Vcpkg! Please take 3 minutes to complete our survey " + "by running: vcpkg contact --survey"); + } + } + } } if (const auto command_function = find_command(Commands::get_available_commands_type_b())) @@ -148,80 +172,41 @@ static void inner(const VcpkgCmdArguments& args) static void load_config() { #if defined(_WIN32) - fs::path localappdata; - { - // Config path in AppDataLocal - wchar_t* localappdatapath = nullptr; - if (S_OK != SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localappdatapath)) __fastfail(1); - localappdata = localappdatapath; - CoTaskMemFree(localappdatapath); - } + auto& fs = Files::get_real_filesystem(); - std::string user_id, user_time, user_mac; - try - { - auto maybe_pghs = Paragraphs::get_paragraphs(Files::get_real_filesystem(), localappdata / "vcpkg" / "config"); - if (const auto p_pghs = maybe_pghs.get()) - { - const auto& pghs = *p_pghs; - - std::unordered_map keys; - if (pghs.size() > 0) keys = pghs[0]; - - for (size_t x = 1; x < pghs.size(); ++x) - { - for (auto&& p : pghs[x]) - keys.insert(p); - } - - user_id = keys["User-Id"]; - user_time = keys["User-Since"]; - user_mac = keys["Mac-Hash"]; - } - } - catch (...) - { - } + auto config = UserConfig::try_read_data(fs); bool write_config = false; // config file not found, could not be read, or invalid - if (user_id.empty() || user_time.empty()) + if (config.user_id.empty() || config.user_time.empty()) { - ::vcpkg::Metrics::Metrics::init_user_information(user_id, user_time); + ::vcpkg::Metrics::Metrics::init_user_information(config.user_id, config.user_time); write_config = true; } - if (user_mac.empty()) + if (config.user_mac.empty()) { - user_mac = Metrics::get_MAC_user(); + config.user_mac = Metrics::get_MAC_user(); write_config = true; } { auto locked_metrics = Metrics::g_metrics.lock(); - locked_metrics->set_user_information(user_id, user_time); - locked_metrics->track_property("user_mac", user_mac); + locked_metrics->set_user_information(config.user_id, config.user_time); + locked_metrics->track_property("user_mac", config.user_mac); } + if (config.last_completed_survey.empty()) + { + config.last_completed_survey = config.user_time; + } + + GlobalState::g_surveydate.lock()->assign(config.last_completed_survey); + if (write_config) { - try - { - std::error_code ec; - auto& fs = Files::get_real_filesystem(); - fs.create_directory(localappdata / "vcpkg", ec); - fs.write_contents(localappdata / "vcpkg" / "config", - Strings::format("User-Id: %s\n" - "User-Since: %s\n" - "Mac-Hash: %s\n", - user_id, - user_time, - user_mac)); - } - catch (...) - { - } + config.try_write_data(fs); } #endif } diff --git a/toolsrc/src/vcpkg/base/chrono.cpp b/toolsrc/src/vcpkg/base/chrono.cpp index f0e45023128..03c1ecce942 100644 --- a/toolsrc/src/vcpkg/base/chrono.cpp +++ b/toolsrc/src/vcpkg/base/chrono.cpp @@ -60,4 +60,66 @@ namespace vcpkg::Chrono std::string ElapsedTime::to_string() const { return format_time_userfriendly(as()); } std::string ElapsedTimer::to_string() const { return elapsed().to_string(); } + + Optional CTime::get_current_date_time() + { + CTime ret; + +#if defined(_WIN32) + struct _timeb timebuffer; + + _ftime_s(&timebuffer); + + const errno_t err = gmtime_s(&ret.m_tm, &timebuffer.time); + + if (err) + { + return nullopt; + } +#else + time_t now = {0}; + time(&now); + auto null_if_failed = gmtime_r(&now, &ret.m_tm); + if (null_if_failed == nullptr) + { + return nullopt; + } +#endif + + return ret; + } + + Optional CTime::parse(CStringView str) + { + CTime ret; + auto assigned = sscanf_s(str.c_str(), + "%d-%d-%dT%d:%d:%d.", + &ret.m_tm.tm_year, + &ret.m_tm.tm_mon, + &ret.m_tm.tm_mday, + &ret.m_tm.tm_hour, + &ret.m_tm.tm_min, + &ret.m_tm.tm_sec); + if (assigned != 6) return nullopt; + if (ret.m_tm.tm_year < 1900) return nullopt; + ret.m_tm.tm_year -= 1900; + if (ret.m_tm.tm_mon < 1) return nullopt; + ret.m_tm.tm_mon -= 1; + mktime(&ret.m_tm); + return ret; + } + + std::string CTime::to_string() const + { + std::array date; + date.fill(0); + + strftime(&date[0], date.size(), "%Y-%m-%dT%H:%M:%S.0Z", &m_tm); + return &date[0]; + } + std::chrono::system_clock::time_point CTime::to_time_point() const + { + auto t = mktime(&m_tm); + return std::chrono::system_clock::from_time_t(t); + } } diff --git a/toolsrc/src/vcpkg/commands.contact.cpp b/toolsrc/src/vcpkg/commands.contact.cpp index 07dcea80ef9..8063fe31733 100644 --- a/toolsrc/src/vcpkg/commands.contact.cpp +++ b/toolsrc/src/vcpkg/commands.contact.cpp @@ -1,8 +1,10 @@ #include "pch.h" +#include #include #include #include +#include namespace vcpkg::Commands::Contact { @@ -28,6 +30,17 @@ namespace vcpkg::Commands::Contact if (Util::Sets::contains(parsed_args.switches, switches[0].name)) { +#if defined(_WIN32) + auto maybe_now = Chrono::CTime::get_current_date_time(); + if (auto p_now = maybe_now.get()) + { + auto& fs = Files::get_real_filesystem(); + auto config = UserConfig::try_read_data(fs); + config.last_completed_survey = p_now->to_string(); + config.try_write_data(fs); + } +#endif + System::cmd_execute("start https://aka.ms/NPS_vcpkg"); System::println("Default browser launched to https://aka.ms/NPS_vcpkg, thank you for your feedback!"); } diff --git a/toolsrc/src/vcpkg/globalstate.cpp b/toolsrc/src/vcpkg/globalstate.cpp index 149401b2c14..123c77d46ad 100644 --- a/toolsrc/src/vcpkg/globalstate.cpp +++ b/toolsrc/src/vcpkg/globalstate.cpp @@ -5,6 +5,8 @@ namespace vcpkg { Util::LockGuarded GlobalState::timer; + Util::LockGuarded GlobalState::g_surveydate; + std::atomic GlobalState::debugging(false); std::atomic GlobalState::feature_packages(false); diff --git a/toolsrc/src/vcpkg/metrics.cpp b/toolsrc/src/vcpkg/metrics.cpp index 69160705cf8..a0d40e7d306 100644 --- a/toolsrc/src/vcpkg/metrics.cpp +++ b/toolsrc/src/vcpkg/metrics.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -15,32 +16,13 @@ namespace vcpkg::Metrics static std::string get_current_date_time() { - struct tm newtime; - std::array date; - date.fill(0); - -#if defined(_WIN32) - struct _timeb timebuffer; - - _ftime_s(&timebuffer); - time_t now = timebuffer.time; - const int milli = timebuffer.millitm; - - const errno_t err = gmtime_s(&newtime, &now); - - if (err) + auto maybe_time = Chrono::CTime::get_current_date_time(); + if (auto ptime = maybe_time.get()) { - return ""; + return ptime->to_string(); } -#else - time_t now; - time(&now); - gmtime_r(&now, &newtime); - const int milli = 0; -#endif - strftime(&date[0], date.size(), "%Y-%m-%dT%H:%M:%S", &newtime); - return std::string(&date[0]) + "." + std::to_string(milli) + "Z"; + return ""; } static std::string generate_random_UUID() diff --git a/toolsrc/src/vcpkg/userconfig.cpp b/toolsrc/src/vcpkg/userconfig.cpp new file mode 100644 index 00000000000..d13a46f4137 --- /dev/null +++ b/toolsrc/src/vcpkg/userconfig.cpp @@ -0,0 +1,83 @@ +#include "pch.h" + +#include +#include +#include +#include + +namespace +{ + static vcpkg::Lazy s_localappdata; + + static const fs::path& get_localappdata() + { + return s_localappdata.get_lazy([]() { + fs::path localappdata; + { + // Config path in AppDataLocal + wchar_t* localappdatapath = nullptr; + if (S_OK != SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localappdatapath)) __fastfail(1); + localappdata = localappdatapath; + CoTaskMemFree(localappdatapath); + } + return localappdata; + }); + } +} + +namespace vcpkg +{ + UserConfig UserConfig::try_read_data(const Files::Filesystem& fs) + { + UserConfig ret; + + try + { + auto maybe_pghs = Paragraphs::get_paragraphs(fs, get_localappdata() / "vcpkg" / "config"); + if (const auto p_pghs = maybe_pghs.get()) + { + const auto& pghs = *p_pghs; + + std::unordered_map keys; + if (pghs.size() > 0) keys = pghs[0]; + + for (size_t x = 1; x < pghs.size(); ++x) + { + for (auto&& p : pghs[x]) + keys.insert(p); + } + + ret.user_id = keys["User-Id"]; + ret.user_time = keys["User-Since"]; + ret.user_mac = keys["Mac-Hash"]; + ret.last_completed_survey = keys["Survey-Completed"]; + } + } + catch (...) + { + } + + return ret; + } + + void UserConfig::try_write_data(Files::Filesystem& fs) const + { + try + { + std::error_code ec; + fs.create_directory(get_localappdata() / "vcpkg", ec); + fs.write_contents(get_localappdata() / "vcpkg" / "config", + Strings::format("User-Id: %s\n" + "User-Since: %s\n" + "Mac-Hash: %s\n" + "Survey-Completed: %s\n", + user_id, + user_time, + user_mac, + last_completed_survey)); + } + catch (...) + { + } + } +} diff --git a/toolsrc/vcpkglib/vcpkglib.vcxproj b/toolsrc/vcpkglib/vcpkglib.vcxproj index ae332e01565..9a7ad6dc0ae 100644 --- a/toolsrc/vcpkglib/vcpkglib.vcxproj +++ b/toolsrc/vcpkglib/vcpkglib.vcxproj @@ -178,6 +178,7 @@ + @@ -240,6 +241,7 @@ + diff --git a/toolsrc/vcpkglib/vcpkglib.vcxproj.filters b/toolsrc/vcpkglib/vcpkglib.vcxproj.filters index e902bffbbf4..966fc7fb952 100644 --- a/toolsrc/vcpkglib/vcpkglib.vcxproj.filters +++ b/toolsrc/vcpkglib/vcpkglib.vcxproj.filters @@ -192,6 +192,9 @@ Source Files\vcpkg\base + + Source Files\vcpkg + @@ -332,5 +335,8 @@ Header Files\vcpkg + + Header Files\vcpkg + \ No newline at end of file diff --git a/toolsrc/vcpkgtest/vcpkgtest.vcxproj b/toolsrc/vcpkgtest/vcpkgtest.vcxproj index 9eafc1adad0..166216c4520 100644 --- a/toolsrc/vcpkgtest/vcpkgtest.vcxproj +++ b/toolsrc/vcpkgtest/vcpkgtest.vcxproj @@ -20,6 +20,7 @@ + diff --git a/toolsrc/vcpkgtest/vcpkgtest.vcxproj.filters b/toolsrc/vcpkgtest/vcpkgtest.vcxproj.filters index 2121f97824c..422f9298e98 100644 --- a/toolsrc/vcpkgtest/vcpkgtest.vcxproj.filters +++ b/toolsrc/vcpkgtest/vcpkgtest.vcxproj.filters @@ -42,6 +42,9 @@ Source Files + + Source Files +