Loading...
Searching...
No Matches
version_check.hpp
Go to the documentation of this file.
// SPDX-FileCopyrightText: 2006-2025 Knut Reinert & Freie Universität Berlin
// SPDX-FileCopyrightText: 2016-2025 Knut Reinert & MPI für molekulare Genetik
// SPDX-License-Identifier: BSD-3-Clause
#pragma once
#include <array>
#include <chrono>
#include <fstream>
#include <future>
#include <iostream>
#include <optional>
#include <regex>
#include <seqan3/std/charconv>
#include <seqan3/argument_parser/auxiliary.hpp>
#include <seqan3/argument_parser/detail/terminal.hpp>
#include <seqan3/io/detail/misc.hpp>
#include <seqan3/io/detail/safe_filesystem_entry.hpp>
#include <seqan3/version.hpp>
#include <sys/stat.h>
namespace seqan3::detail
{
// ---------------------------------------------------------------------------------------------------------------------
// function call_server()
// ---------------------------------------------------------------------------------------------------------------------
inline void call_server(std::string const & command, std::promise<bool> prom)
{
// system call - http response is stored in a file '.config/seqan/{appname}_version'
if (system(command.c_str()))
prom.set_value(false);
else
prom.set_value(true);
}
// ---------------------------------------------------------------------------------------------------------------------
// version_checker
// ---------------------------------------------------------------------------------------------------------------------
class version_checker
{
public:
version_checker() = delete;
version_checker(version_checker const &) = default;
version_checker & operator=(version_checker const &) = default;
version_checker(version_checker &&) = default;
version_checker & operator=(version_checker &&) = default;
~version_checker() = default;
version_checker(std::string name_, std::string const & version_, std::string const & app_url = std::string{}) :
name{std::move(name_)}
{
assert(std::regex_match(name, std::regex{"^[a-zA-Z0-9_-]+$"})); // check on construction of the argument parser
if (!app_url.empty())
{
message_app_update.pop_back(); // remove second newline
message_app_update.append("[APP INFO] :: Visit " + app_url + " for updates.\n\n");
}
#if defined(NDEBUG)
timestamp_filename = cookie_path / (name + "_usr.timestamp");
#else
timestamp_filename = cookie_path / (name + "_dev.timestamp");
#endif
std::smatch versionMatch;
// Ensure version string is not corrupt
if (!version_.empty() && /*regex allows version prefix instead of exact match */
std::regex_search(version_, versionMatch, std::regex("^([[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+).*")))
{
version = versionMatch.str(1); // in case the git revision number is given take only version number
}
}
void operator()(std::promise<bool> prom)
{
std::array<int, 3> empty_version{0, 0, 0};
std::array<int, 3> srv_app_version{};
std::array<int, 3> srv_seqan_version{};
std::ifstream version_file{cookie_path / (name + ".version")};
if (version_file.is_open())
{
std::string line{};
std::getline(version_file, line); // get first line which should only contain the version number of the app
if (line != unregistered_app)
srv_app_version = get_numbers_from_version_string(line);
#if !defined(NDEBUG)
else
std::cerr << message_unregistered_app;
#endif // !defined(NDEBUG)
std::getline(version_file, line); // get second line which should only contain the version number of seqan
srv_seqan_version = get_numbers_from_version_string(line);
version_file.close();
}
#if !defined(NDEBUG) // only check seqan version in debug
if (srv_seqan_version != empty_version)
{
std::array<int, 3> seqan_version = {SEQAN3_VERSION_MAJOR, SEQAN3_VERSION_MINOR, SEQAN3_VERSION_PATCH};
if (seqan_version < srv_seqan_version)
std::cerr << message_seqan3_update;
}
#endif
if (srv_app_version != empty_version) // app version
{
#if defined(NDEBUG) // only check app version in release
if (get_numbers_from_version_string(version) < srv_app_version)
std::cerr << message_app_update;
#endif // defined(NDEBUG)
#if !defined(NDEBUG) // only notify developer that app version should be updated on server
if (get_numbers_from_version_string(version) > srv_app_version)
std::cerr << message_registered_app_update;
#endif // !defined(NDEBUG)
}
std::cerr << std::flush;
std::string program = get_program();
if (program.empty())
{
prom.set_value(false);
return;
}
// 'cookie_path' is no user input and `name` is escaped on construction of the argument parser.
std::filesystem::path out_file = cookie_path / (name + ".version");
// build up command for server call
std::string command = program + // no user defined input
" " + out_file.string() + " "
+ std::string{"https://seqan-update.cs.uni-tuebingen.de/check/SeqAn3_"} +
#ifdef __linux
"Linux" +
#elif __APPLE__
"MacOS" +
#elif defined(_WIN32)
"Windows" +
#elif __FreeBSD__
"FreeBSD" +
#elif __OpenBSD__
"OpenBSD" +
#else
"unknown" +
#endif
#if __x86_64__ || __ppc64__
"_64_" +
#else
"_32_" +
#endif
name + // !user input! escaped on construction of the argument parser
"_" + version + // !user input! escaped on construction of the version_checker
#if defined(_WIN32)
"; exit [int] -not $?}\" > nul 2>&1";
#else
" > /dev/null 2>&1";
#endif
// launch a separate thread to not defer runtime.
std::thread(call_server, command, std::move(prom)).detach();
}
static std::filesystem::path get_path()
{
using namespace std::filesystem;
path tmp_path;
tmp_path = std::string{getenv(home_env_name)};
tmp_path /= ".config";
// First, create .config if it does not already exist.
std::error_code err;
create_directory(tmp_path, err);
// If this did not fail we, create the seqan subdirectory.
if (!err)
{
tmp_path /= "seqan";
create_directory(tmp_path, err);
}
// .config/seqan cannot be created, try tmp directory.
if (err)
tmp_path = temp_directory_path(); // choose temp dir instead
// check if files can be written inside dir
path dummy = tmp_path / "dummy.txt";
std::ofstream file{dummy};
detail::safe_filesystem_entry file_guard{dummy};
bool is_open = file.is_open();
bool is_good = file.good();
file.close();
file_guard.remove_no_throw();
if (!is_good || !is_open) // no write permissions
{
tmp_path.clear(); // empty path signals no available directory to write to, version check will not be done
}
return tmp_path;
}
bool decide_if_check_is_performed(update_notifications developer_approval, std::optional<bool> user_approval)
{
if (developer_approval == update_notifications::off)
return false;
if (std::getenv("SEQAN3_NO_VERSION_CHECK") != nullptr) // environment variable was set
return false;
if (user_approval.has_value())
return user_approval.value();
// version check was not explicitly handled so let's check the cookie
if (std::filesystem::exists(cookie_path))
{
std::ifstream timestamp_file{timestamp_filename};
std::string cookie_line{};
if (timestamp_file.is_open())
{
std::getline(timestamp_file, cookie_line); // first line contains the timestamp
if (get_time_diff_to_current(cookie_line) < 86400 /*one day in seconds*/)
{
return false;
}
std::getline(timestamp_file, cookie_line); // second line contains the last user decision
if (cookie_line == "NEVER")
{
return false;
}
else if (cookie_line == "ALWAYS")
{
return true;
}
// else we do not return but continue to ask the user
timestamp_file.close();
}
}
// Up until now, the user did not specify the --version-check option, the environment variable was not set,
// nor did the the cookie tell us what to do. We will now ask the user if possible or do the check by default.
write_cookie("ASK"); // Ask again next time when we read the cookie, if this is not overwritten.
if (detail::is_terminal()) // LCOV_EXCL_START
{
std::cerr << R"(
#######################################################################
Automatic Update Notifications
#######################################################################
This app can look for updates automatically in the background,
do you want to do that?
[a] Always perform version checks for this app.
[n] Never perform version checks for this app.
[y] Yes, perform a version check now, and ask again tomorrow.
[s] Skip the version check now, but ask again tomorrow (the default).
Please enter one of [a, n, y, s] and press [RETURN].
For more information, see:
https://github.com/seqan/seqan3/wiki/Update-Notifications
#######################################################################
)";
std::string line{};
std::getline(std::cin, line);
line.resize(1, 's'); // ignore everything but the first char or resizes the empty string to the default
switch (line[0])
{
case 'y':
{
return true;
}
case 'a':
{
write_cookie(std::string{"ALWAYS"}); // overwrite cookie
return true;
}
case 'n':
{
write_cookie(std::string{"NEVER"}); // overwrite cookie
return false;
}
default:
{
return false;
}
}
}
else // if !detail::is_terminal()
{
std::cerr << R"(
#######################################################################
Automatic Update Notifications
#######################################################################
This app performs automatic checks for updates. For more information
see: https://github.com/seqan/seqan3/wiki/Update-Notifications
#######################################################################
)";
return true; // default: check version if you cannot ask the user
}
} // LCOV_EXCL_STOP
static constexpr std::string_view unregistered_app = "UNREGISTERED_APP";
static constexpr std::string_view message_seqan3_update =
"[SEQAN3 INFO] :: A new SeqAn version is available online.\n"
"[SEQAN3 INFO] :: Please visit www.github.com/seqan/seqan3.git for an update\n"
"[SEQAN3 INFO] :: or inform the developer of this app.\n"
"[SEQAN3 INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
static constexpr std::string_view message_unregistered_app =
"[SEQAN3 INFO] :: Thank you for using SeqAn!\n"
"[SEQAN3 INFO] :: Do you wish to register your app for update notifications?\n"
"[SEQAN3 INFO] :: Just send an email to support@seqan.de with your app name and version number.\n"
"[SEQAN3 INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
static constexpr std::string_view message_registered_app_update =
"[APP INFO] :: We noticed the app version you use is newer than the one registered with us.\n"
"[APP INFO] :: Please send us an email with the new version so we can correct it (support@seqan.de)\n\n";
std::string message_app_update =
"[APP INFO] :: A new version of this application is now available.\n"
"[APP INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
/*Might be extended if a url is given on construction.*/
static constexpr char const * home_env_name{
#if defined(_WIN32)
"UserProfile"
#else
"HOME"
#endif
};
std::string name;
std::string version{"0.0.0"};
std::regex version_regex{"^[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+$"};
std::filesystem::path cookie_path = get_path();
std::filesystem::path timestamp_filename;
private:
static std::string get_program()
{
#if defined(_WIN32)
return "powershell.exe -NoLogo -NonInteractive -Command \"& {Invoke-WebRequest -erroraction 'silentlycontinue' "
"-OutFile";
#else // Unix based platforms.
if (!system("/usr/bin/env -i wget --version > /dev/null 2>&1"))
return "/usr/bin/env -i wget --timeout=10 --tries=1 -q -O";
else if (!system("/usr/bin/env -i curl --version > /dev/null 2>&1"))
return "/usr/bin/env -i curl --connect-timeout 10 -o";
// In case neither wget nor curl is available try ftp/fetch if system is OpenBSD/FreeBSD.
// Note, both systems have ftp/fetch command installed by default so we do not guard against it.
# if defined(__OpenBSD__)
return "/usr/bin/env -i ftp -w10 -Vo";
# elif defined(__FreeBSD__)
return "/usr/bin/env -i fetch --timeout=10 -o";
# else
return "";
# endif // __OpenBSD__
#endif // defined(_WIN32)
}
double get_time_diff_to_current(std::string const & str_time) const
{
namespace co = std::chrono;
double curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
double d_time{};
std::from_chars(str_time.data(), str_time.data() + str_time.size(), d_time);
return curr - d_time;
}
std::array<int, 3> get_numbers_from_version_string(std::string const & str) const
{
std::array<int, 3> result{};
if (!std::regex_match(str, version_regex))
return result;
auto res = std::from_chars(str.data(), str.data() + str.size(), result[0]); // stops and sets res.ptr at '.'
res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[1]);
res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[2]);
return result;
}
template <typename msg_type>
void write_cookie(msg_type && msg)
{
// The current time
namespace co = std::chrono;
auto curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
std::ofstream timestamp_file{timestamp_filename};
if (timestamp_file.is_open())
{
timestamp_file << curr << '\n' << msg;
timestamp_file.close();
}
}
};
} // namespace seqan3::detail
array
auxiliary.hpp
Provides auxiliary information.
std::string::c_str
T c_str(T... args)
charconv
The <charconv> header from C++17's standard library.
chrono
std::filesystem::path::clear
T clear(T... args)
std::ifstream::close
T close(T... args)
std::experimental::filesystem::create_directory
T create_directory(T... args)
std::string::data
T data(T... args)
std::string::empty
T empty(T... args)
std::filesystem::exists
T exists(T... args)
std::flush
T flush(T... args)
fstream
future
std::getenv
T getenv(T... args)
std::getline
T getline(T... args)
std::ofstream::good
T good(T... args)
misc.hpp
Provides various utility functions.
iostream
std::ifstream::is_open
T is_open(T... args)
seqan3::update_notifications
update_notifications
Indicates whether application allows automatic update notifications by the seqan3::argument_parser.
Definition auxiliary.hpp:268
seqan3::update_notifications::off
@ off
Automatic update notifications should be disabled.
Definition auxiliary.hpp:270
std::from_chars
from_chars_result from_chars(char const *first, char const *last, floating_point_type &value, chars_format fmt=chars_format::general) noexcept
Parse a char sequence into an floating point value.
Definition charconv:252
std::optional::has_value
T has_value(T... args)
optional
regex
std::regex_match
T regex_match(T... args)
std::regex_search
T regex_search(T... args)
std::string::resize
T resize(T... args)
safe_filesystem_entry.hpp
Provides seqan3::detail::safe_filesystem_entry.
std::promise::set_value
T set_value(T... args)
std::string::size
T size(T... args)
std::smatch::str
T str(T... args)
std::filesystem::path::string
T string(T... args)
std::system
T system(T... args)
std::experimental::filesystem::temp_directory_path
T temp_directory_path(T... args)
terminal.hpp
Checks if program is run interactively and retrieves dimensions of terminal (Transferred from seqan2)...
std::optional::value
T value(T... args)
version.hpp
Provides SeqAn version macros and global variables.
SEQAN3_VERSION_MAJOR
#define SEQAN3_VERSION_MAJOR
The major version as MACRO.
Definition version.hpp:16
SEQAN3_VERSION_PATCH
#define SEQAN3_VERSION_PATCH
The patch version as MACRO.
Definition version.hpp:20
SEQAN3_VERSION_MINOR
#define SEQAN3_VERSION_MINOR
The minor version as MACRO.
Definition version.hpp:18
Hide me