Sharg 1.2.3-rc.1
The argument parser for bio-c++ tools.
Loading...
Searching...
No Matches
version_check.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2006-2026, Knut Reinert & Freie Universität Berlin
2// SPDX-FileCopyrightText: 2016-2026, Knut Reinert & MPI für molekulare Genetik
3// SPDX-License-Identifier: BSD-3-Clause
4
10#pragma once
11
12#include <sharg/std/charconv>
13#include <array>
14#include <cassert>
15#include <filesystem>
16#include <fstream>
17#include <future>
18#include <iostream>
19#include <optional>
20#include <regex>
21
22#include <sharg/auxiliary.hpp>
24
25namespace sharg::detail
26{
27
28// ------------------------------------------------------------------------------------------------------------------
29// function call_server()
30// ------------------------------------------------------------------------------------------------------------------
31
39inline void call_server(std::string const & command, std::promise<bool> prom)
40{
41 // system call - http response is stored in a file '.config/seqan/{appname}_version'
42 if (system(command.c_str()))
43 prom.set_value(false);
44 else
45 prom.set_value(true);
46}
47
48// ------------------------------------------------------------------------------------------------------------------
49// function quote_argument()
50// ------------------------------------------------------------------------------------------------------------------
51
59inline std::string quote_argument(std::string const & argument)
60{
61#if defined(_WIN32)
62 // The argument is part of a PowerShell command, which escapes a single quote by doubling it.
63 // Windows is not supported. If it ever is, this needs to be adapted: PowerShell also treats the typographic quotes
64 // U+2018 to U+201B as single quotes, and cmd.exe, which runs the command, expands %VARIABLE% even within quotes.
65 constexpr std::string_view escaped_quote{"''"};
66#else
67 // A POSIX shell cannot escape a single quote within single quotes: close the quotes, add an escaped quote, reopen.
68 constexpr std::string_view escaped_quote{"'\\''"};
69#endif
70
71 std::string result{'\''};
72
73 for (char const c : argument)
74 {
75 if (c == '\'')
76 result.append(escaped_quote);
77 else
78 result.push_back(c);
79 }
80
81 result.push_back('\'');
82 return result;
83}
84
85// ------------------------------------------------------------------------------------------------------------------
86// version_checker
87// ------------------------------------------------------------------------------------------------------------------
88
93{
94public:
99 version_checker() = delete;
100 version_checker(version_checker const &) = default;
104 ~version_checker() = default;
105
111 version_checker(std::string name_, std::string const & version_, std::string const & app_url = std::string{}) :
112 name{std::move(name_)}
113 {
114 assert(std::regex_match(name, std::regex{"^[a-zA-Z0-9_-]+$"})); // check on construction of the parser
115
116 if (!app_url.empty())
117 {
118 message_app_update.pop_back(); // remove second newline
119 message_app_update.append("[APP VERSION INFO] :: Visit " + app_url + " for updates.\n\n");
120 }
121
122#if defined(NDEBUG)
123 timestamp_filename = cookie_path / (name + "_usr.timestamp");
124#else
125 timestamp_filename = cookie_path / (name + "_dev.timestamp");
126#endif
127 std::smatch versionMatch;
128
129 // Ensure version string is not corrupt
130 if (!version_.empty() && /*regex allows version prefix instead of exact match */
131 std::regex_search(version_, versionMatch, std::regex("^([[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+).*")))
132 {
133 version = versionMatch.str(1); // in case the git revision number is given take only version number
134 }
135 }
137
165 {
166 std::array<int, 3> empty_version{0, 0, 0};
167 std::array<int, 3> srv_app_version{};
168 std::array<int, 3> srv_sharg_version{};
169
170 std::ifstream version_file{cookie_path / (name + ".version")};
171
172 if (version_file.is_open())
173 {
174 std::string line{};
175 std::getline(version_file, line); // get first line which should only contain the version number of the app
176
177 if (line != unregistered_app)
178 srv_app_version = get_numbers_from_version_string(line);
179#if !defined(NDEBUG)
180 else
182#endif // !defined(NDEBUG)
183
184 std::getline(version_file, line); // get second line which should only contain the version number of sharg
185 srv_sharg_version = get_numbers_from_version_string(line);
186
187 version_file.close();
188 }
189
190#if !defined(NDEBUG) // only check Sharg version in debug
191 if (srv_sharg_version != empty_version)
192 {
194
195 if (sharg_version < srv_sharg_version)
197 }
198#endif
199
200 if (srv_app_version != empty_version) // app version
201 {
202#if defined(NDEBUG) // only check app version in release
203 if (get_numbers_from_version_string(version) < srv_app_version)
205#endif // defined(NDEBUG)
206
207#if !defined(NDEBUG) // only notify developer that app version should be updated on server
208 if (get_numbers_from_version_string(version) > srv_app_version)
210#endif // !defined(NDEBUG)
211 }
212
214
215 std::string program = get_program();
216
217 if (program.empty())
218 {
219 prom.set_value(false);
220 return;
221 }
222
223 // 'cookie_path' is derived from environment variables and may contain any character, e.g. spaces.
224 std::filesystem::path out_file = cookie_path / (name + ".version");
225
226 // build up command for server call
227 std::string command = program + // no user defined input
228 " " + quote_argument(out_file.string()) + " "
229 + std::string{"https://seqan-update.cs.uni-tuebingen.de/check/SeqAn-Sharg_"} +
230#ifdef __linux
231 "Linux" +
232#elif __APPLE__
233 "MacOS" +
234#elif defined(_WIN32)
235 "Windows" +
236#elif __FreeBSD__
237 "FreeBSD" +
238#elif __OpenBSD__
239 "OpenBSD" +
240#else
241 "unknown" +
242#endif
243#if __x86_64__ || __ppc64__
244 "_64_" +
245#else
246 "_32_" +
247#endif
248 name + // !user input! escaped on construction of the parser
249 "_" + version + // !user input! escaped on construction of the version_checker
250#if defined(_WIN32)
251 "; exit [int] -not $?}\" > nul 2>&1";
252#else
253 " > /dev/null 2>&1";
254#endif
255
256 // launch a separate thread to not defer runtime.
257 std::thread(call_server, command, std::move(prom)).detach();
258 }
259
267 {
268 using namespace std::filesystem;
269
270 path tmp_path;
271
272 if (char * home_env = getenv(home_env_name); home_env != nullptr)
273 tmp_path = std::string{home_env};
274 tmp_path /= ".config";
275
276 // First, create .config if it does not already exist.
277 std::error_code err;
278 create_directory(tmp_path, err);
279
280 // If this did not fail we, create the seqan subdirectory.
281 if (!err)
282 {
283 tmp_path /= "seqan"; // sharg is part of seqan, so the naming is fine.
284 create_directory(tmp_path, err);
285 }
286
287 // .config/seqan cannot be created, try tmp directory.
288 if (err)
289 {
290 tmp_path = temp_directory_path(err); // choose temp dir instead
291
292 if (err) // e.g., TMPDIR is not a directory
293 {
294 tmp_path.clear(); // empty path signals no available directory to write to
295 return tmp_path;
296 }
297 }
298
299 // Check if files can be written inside dir. An existing file, e.g., in the temporary directory, might belong to
300 // someone else and must not be opened. Hence, the probe file gets a unique name and std::ios::noreplace fails
301 // instead of opening an existing file. In the unlikely case of a name clash, the directory is not used.
302 path const dummy =
303 tmp_path
304 / ("dummy_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt");
305 std::ofstream file{dummy, std::ios::out | std::ios::noreplace};
306
307 bool is_open = file.is_open();
308 bool is_good = file.good();
309 file.close();
310
311 if (is_open) // only remove the file if it was created here
312 remove(dummy, err);
313
314 if (!is_good || !is_open) // no write permissions
315 {
316 tmp_path.clear(); // empty path signals no available directory to write to, version check will not be done
317 }
318
319 return tmp_path;
320 }
321
331 static bool is_disabled(update_notifications developer_approval, std::optional<bool> user_approval)
332 {
333 return developer_approval == update_notifications::off || std::getenv("SHARG_NO_VERSION_CHECK") != nullptr
334 || user_approval == false;
335 }
336
365 {
366 if (is_disabled(developer_approval, user_approval))
367 return false;
368
369 if (cookie_path.empty()) // no directory to write the timestamp and version files to
370 {
371 // Like the question whether to perform the version check, the message is only shown interactively.
372 if (detail::stdin_is_terminal() && detail::stderr_is_terminal()) // LCOV_EXCL_START
374 // LCOV_EXCL_STOP
375 return false;
376 }
377
378 if (user_approval.has_value()) // the user explicitly enabled the version check
379 return true;
380
381 // version check was not explicitly handled so let's check the cookie
383 {
384 std::ifstream timestamp_file{timestamp_filename};
385 std::string cookie_line{};
386
387 if (timestamp_file.is_open())
388 {
389 std::getline(timestamp_file, cookie_line); // first line contains the timestamp
390
391 if (get_time_diff_to_current(cookie_line) < 86400 /*one day in seconds*/)
392 {
393 return false;
394 }
395
396 std::getline(timestamp_file, cookie_line); // second line contains the last user decision
397
398 if (cookie_line == "NEVER")
399 {
400 return false;
401 }
402 else if (cookie_line == "ALWAYS")
403 {
404 return true;
405 }
406 // else we do not return but continue to ask the user
407
408 timestamp_file.close();
409 }
410 }
411
412 // Up until now, the user did not specify the --version-check option, the environment variable was not set,
413 // nor did the the cookie tell us what to do. We will now ask the user if possible or do the check by default.
414 write_cookie("ASK"); // Ask again next time when we read the cookie, if this is not overwritten.
415
416 if (detail::stdin_is_terminal() && detail::stderr_is_terminal()) // LCOV_EXCL_START
417 {
418 std::cerr << R"(
419#######################################################################
420 Automatic Update Notifications
421#######################################################################
422
423 This app can look for updates automatically in the background,
424 do you want to do that?
425
426 [a] Always perform version checks for this app.
427 [n] Never perform version checks for this app.
428 [y] Yes, perform a version check now, and ask again tomorrow.
429 [s] Skip the version check now, but ask again tomorrow (the default).
430
431 Please enter one of [a, n, y, s] and press [RETURN].
432
433 For more information, see:
434 https://docs.seqan.de/sharg/main_user/about_update_notifications.html
435
436#######################################################################
437
438)";
439 std::string line{};
440 std::getline(std::cin, line);
441 line.resize(1, 's'); // ignore everything but the first char or resizes the empty string to the default
442
443 switch (line[0])
444 {
445 case 'y':
446 {
447 return true;
448 }
449 case 'a':
450 {
451 write_cookie(std::string{"ALWAYS"}); // overwrite cookie
452 return true;
453 }
454 case 'n':
455 {
456 write_cookie(std::string{"NEVER"}); // overwrite cookie
457 return false;
458 }
459 default:
461 return false;
462 }
463 }
464 }
465 else // of: if (detail::stdin_is_terminal() && detail::stderr_is_terminal())
467 return false; // default: do not check version today, if you cannot ask the user
468 }
469 } // LCOV_EXCL_STOP
470
472 static constexpr std::string_view unregistered_app = "UNREGISTERED_APP";
474 static constexpr std::string_view message_sharg_update =
475 "[SHARG VERSION INFO] :: A new Sharg version is available online.\n"
476 "[SHARG VERSION INFO] :: Please visit www.github.com/seqan/sharg-parser.git for an update\n"
477 "[SHARG VERSION INFO] :: or inform the developer of this app.\n"
478 "[SHARG VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
481 "[SHARG VERSION INFO] :: Thank you for using Sharg!\n"
482 "[SHARG VERSION INFO] :: Do you wish to register your app for update notifications?\n"
483 "[SHARG VERSION INFO] :: Just send an email to support@seqan.de with your app name and version number.\n"
484 "[SHARG VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
487 "[APP VERSION INFO] :: We noticed the app version you use is newer than the one registered with us.\n"
488 "[APP VERSION INFO] :: Please send us an email with the new version so we can correct it "
489 "(support@seqan.de)\n\n";
492 "[SHARG VERSION INFO] :: The version check was skipped because there is no writable directory for its files.\n"
493 "[SHARG VERSION INFO] :: For details, see "
494 "https://docs.seqan.de/sharg/main_user/about_update_notifications.html\n"
495 "[SHARG VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
498 "[APP VERSION INFO] :: A new version of this application is now available.\n"
499 "[APP VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
500 /*Will be extended if a url is given on construction of version_check.*/
503 static constexpr char const * home_env_name
504 {
505#if defined(_WIN32)
506 "UserProfile"
507#else
508 "HOME"
509#endif
510 };
511
515 std::string version{"0.0.0"};
517 std::regex version_regex{"^[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+$"};
522
523private:
525 static std::string get_program()
526 {
527#if defined(_WIN32)
528 return "powershell.exe -NoLogo -NonInteractive -Command \"& {Invoke-WebRequest -erroraction 'silentlycontinue' "
529 "-OutFile";
530#else // Unix based platforms.
531 if (!system("/usr/bin/env -i wget --version > /dev/null 2>&1"))
532 return "/usr/bin/env -i wget --timeout=10 --tries=1 -q -O";
533 else if (!system("/usr/bin/env -i curl --version > /dev/null 2>&1"))
534 return "/usr/bin/env -i curl --connect-timeout 10 -o";
535// In case neither wget nor curl is available try ftp/fetch if system is OpenBSD/FreeBSD.
536// Note, both systems have ftp/fetch command installed by default so we do not guard against it.
537# if defined(__OpenBSD__)
538 return "/usr/bin/env -i ftp -w10 -Vo";
539# elif defined(__FreeBSD__)
540 return "/usr/bin/env -i fetch --timeout=10 -o";
541# else
542 return "";
543# endif // __OpenBSD__
544#endif // defined(_WIN32)
545 }
546
548 double get_time_diff_to_current(std::string const & str_time) const
549 {
550 namespace co = std::chrono;
551 double curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
552
553 double d_time{};
554 std::from_chars(str_time.data(), str_time.data() + str_time.size(), d_time);
555
556 return curr - d_time;
557 }
558
563 {
564 std::array<int, 3> result{};
565
567 return result;
568
569 auto res = std::from_chars(str.data(), str.data() + str.size(), result[0]); // stops and sets res.ptr at '.'
570 res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[1]);
571 res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[2]);
572
573 return result;
574 }
575
580 template <typename msg_type>
581 void write_cookie(msg_type && msg)
582 {
583 // The current time
584 namespace co = std::chrono;
585 auto curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
586
587 std::ofstream timestamp_file{timestamp_filename};
588
589 if (timestamp_file.is_open())
590 {
591 timestamp_file << curr << '\n' << msg;
592 timestamp_file.close();
593 }
594 }
595};
596
597} // namespace sharg::detail
T append(T... args)
Provides auxiliary information.
T c_str(T... args)
The <charconv> header from C++17's standard library.
A functor whose operator() performs the server http request and version checks.
Definition version_check.hpp:93
static constexpr std::string_view message_sharg_update
The message directed to the developer of the app if a new sharg version is available.
Definition version_check.hpp:454
void write_cookie(msg_type &&msg)
Writes a cookie file with a specified message.
Definition version_check.hpp:561
version_checker(std::string name_, std::string const &version_, std::string const &app_url=std::string{})
Initialises the version_checker with the application name and version.
Definition version_check.hpp:111
static constexpr std::string_view message_unregistered_app
The message directed to the developer of the app if the app is not yet registered with us.
Definition version_check.hpp:460
version_checker()=delete
This class has to be initialised with name and version information.
std::string version
The version of the application.
Definition version_check.hpp:495
std::filesystem::path timestamp_filename
The timestamp filename.
Definition version_check.hpp:501
static constexpr std::string_view unregistered_app
The identification string that may appear in the version file if an app is unregistered.
Definition version_check.hpp:452
static std::filesystem::path get_path()
Returns a writable path to store timestamp and version files or an empty path if none exists.
Definition version_check.hpp:266
static std::string get_program()
Returns the command line call as a std::string of an available program depending on the environment.
Definition version_check.hpp:505
std::string name
The application name.
Definition version_check.hpp:493
version_checker & operator=(version_checker const &)=default
Defaulted.
static constexpr std::string_view message_registered_app_update
The message directed to the developer if the application is registered but under a lower version.
Definition version_check.hpp:466
static bool is_disabled(update_notifications developer_approval, std::optional< bool > user_approval)
Whether the developer, the environment variable SHARG_NO_VERSION_CHECK or the user disabled the check...
Definition version_check.hpp:331
version_checker(version_checker const &)=default
Defaulted.
version_checker & operator=(version_checker &&)=default
Defaulted.
std::filesystem::path cookie_path
The path to store timestamp and version files (either ~/.config/seqan or the tmp directory).
Definition version_check.hpp:499
version_checker(version_checker &&)=default
Defaulted.
double get_time_diff_to_current(std::string const &str_time) const
Reads the timestamp file if possible and returns the time difference to the current time.
Definition version_check.hpp:528
void operator()(std::promise< bool > prom)
Initialises the version_checker with the application name and version.
Definition version_check.hpp:164
std::regex version_regex
The regex to verify a valid version string.
Definition version_check.hpp:497
~version_checker()=default
Defaulted.
static constexpr std::string_view message_no_writable_directory
The message if the version check is skipped because there is no writable directory for its files.
Definition version_check.hpp:471
std::string message_app_update
The message directed to the user of the app if a new app version is available.
Definition version_check.hpp:477
std::array< int, 3 > get_numbers_from_version_string(std::string const &str) const
Parses a version string into an array of length 3.
Definition version_check.hpp:542
static constexpr char const * home_env_name
The environment name of the home environment used by getenv()
Definition version_check.hpp:484
bool decide_if_check_is_performed(update_notifications developer_approval, std::optional< bool > user_approval)
The central decision whether to perform the version check or not.
Definition version_check.hpp:364
T data(T... args)
T detach(T... args)
T empty(T... args)
T exists(T... args)
T flush(T... args)
T from_chars(T... args)
T getenv(T... args)
T getline(T... args)
update_notifications
Indicates whether application allows automatic update notifications by the sharg::parser.
Definition auxiliary.hpp:27
@ off
Automatic update notifications should be disabled.
bool stderr_is_terminal()
Check whether the standard error output is interactive.
Definition terminal.hpp:63
bool stdin_is_terminal()
Check whether the standard input is interactive.
Definition terminal.hpp:33
std::string quote_argument(std::string const &argument)
Quotes a string such that the shell executing the server call treats it as a single,...
Definition version_check.hpp:59
void call_server(std::string const &command, std::promise< bool > prom)
Writes a timestamp file and performs the server call to get the newest version information.
Definition version_check.hpp:39
T has_value(T... args)
T pop_back(T... args)
T regex_match(T... args)
T regex_search(T... args)
T set_value(T... args)
T size(T... args)
T str(T... args)
Checks if program is run interactively and retrieves dimensions of terminal (Transferred from seqan2)...
T to_string(T... args)
#define SHARG_VERSION_MINOR
The minor version as MACRO.
Definition version.hpp:18
#define SHARG_VERSION_PATCH
The patch version as MACRO.
Definition version.hpp:20
constexpr std::size_t sharg_version
The full version as std::size_t.
Definition version.hpp:61
#define SHARG_VERSION_MAJOR
The major version as MACRO.
Definition version.hpp:16
Hide me