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 <fstream>
15#include <future>
16#include <iostream>
17#include <optional>
18#include <regex>
19
20#include <sharg/auxiliary.hpp>
23
24namespace sharg::detail
25{
26
27// ------------------------------------------------------------------------------------------------------------------
28// function call_server()
29// ------------------------------------------------------------------------------------------------------------------
30
38inline void call_server(std::string const & command, std::promise<bool> prom)
39{
40 // system call - http response is stored in a file '.config/seqan/{appname}_version'
41 if (system(command.c_str()))
42 prom.set_value(false);
43 else
44 prom.set_value(true);
45}
46
47// ------------------------------------------------------------------------------------------------------------------
48// version_checker
49// ------------------------------------------------------------------------------------------------------------------
50
55{
56public:
61 version_checker() = delete;
62 version_checker(version_checker const &) = default;
66 ~version_checker() = default;
67
73 version_checker(std::string name_, std::string const & version_, std::string const & app_url = std::string{}) :
74 name{std::move(name_)}
75 {
76 assert(std::regex_match(name, std::regex{"^[a-zA-Z0-9_-]+$"})); // check on construction of the parser
77
78 if (!app_url.empty())
79 {
80 message_app_update.pop_back(); // remove second newline
81 message_app_update.append("[APP VERSION INFO] :: Visit " + app_url + " for updates.\n\n");
82 }
83
84#if defined(NDEBUG)
85 timestamp_filename = cookie_path / (name + "_usr.timestamp");
86#else
87 timestamp_filename = cookie_path / (name + "_dev.timestamp");
88#endif
89 std::smatch versionMatch;
90
91 // Ensure version string is not corrupt
92 if (!version_.empty() && /*regex allows version prefix instead of exact match */
93 std::regex_search(version_, versionMatch, std::regex("^([[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+).*")))
94 {
95 version = versionMatch.str(1); // in case the git revision number is given take only version number
96 }
97 }
99
127 {
128 std::array<int, 3> empty_version{0, 0, 0};
129 std::array<int, 3> srv_app_version{};
130 std::array<int, 3> srv_sharg_version{};
131
132 std::ifstream version_file{cookie_path / (name + ".version")};
133
134 if (version_file.is_open())
135 {
136 std::string line{};
137 std::getline(version_file, line); // get first line which should only contain the version number of the app
138
139 if (line != unregistered_app)
140 srv_app_version = get_numbers_from_version_string(line);
141#if !defined(NDEBUG)
142 else
144#endif // !defined(NDEBUG)
145
146 std::getline(version_file, line); // get second line which should only contain the version number of sharg
147 srv_sharg_version = get_numbers_from_version_string(line);
148
149 version_file.close();
150 }
151
152#if !defined(NDEBUG) // only check Sharg version in debug
153 if (srv_sharg_version != empty_version)
154 {
156
157 if (sharg_version < srv_sharg_version)
159 }
160#endif
161
162 if (srv_app_version != empty_version) // app version
163 {
164#if defined(NDEBUG) // only check app version in release
165 if (get_numbers_from_version_string(version) < srv_app_version)
167#endif // defined(NDEBUG)
168
169#if !defined(NDEBUG) // only notify developer that app version should be updated on server
170 if (get_numbers_from_version_string(version) > srv_app_version)
172#endif // !defined(NDEBUG)
173 }
174
176
177 std::string program = get_program();
178
179 if (program.empty())
180 {
181 prom.set_value(false);
182 return;
183 }
184
185 // 'cookie_path' is no user input and `name` is escaped on construction of the parser.
186 std::filesystem::path out_file = cookie_path / (name + ".version");
187
188 // build up command for server call
189 std::string command = program + // no user defined input
190 " " + out_file.string() + " "
191 + std::string{"https://seqan-update.cs.uni-tuebingen.de/check/SeqAn-Sharg_"} +
192#ifdef __linux
193 "Linux" +
194#elif __APPLE__
195 "MacOS" +
196#elif defined(_WIN32)
197 "Windows" +
198#elif __FreeBSD__
199 "FreeBSD" +
200#elif __OpenBSD__
201 "OpenBSD" +
202#else
203 "unknown" +
204#endif
205#if __x86_64__ || __ppc64__
206 "_64_" +
207#else
208 "_32_" +
209#endif
210 name + // !user input! escaped on construction of the parser
211 "_" + version + // !user input! escaped on construction of the version_checker
212#if defined(_WIN32)
213 "; exit [int] -not $?}\" > nul 2>&1";
214#else
215 " > /dev/null 2>&1";
216#endif
217
218 // launch a separate thread to not defer runtime.
219 std::thread(call_server, command, std::move(prom)).detach();
220 }
221
224 {
225 using namespace std::filesystem;
226
227 path tmp_path;
228
229 if (char * home_env = getenv(home_env_name); home_env != nullptr)
230 tmp_path = std::string{home_env};
231 tmp_path /= ".config";
232
233 // First, create .config if it does not already exist.
234 std::error_code err;
235 create_directory(tmp_path, err);
236
237 // If this did not fail we, create the seqan subdirectory.
238 if (!err)
239 {
240 tmp_path /= "seqan"; // sharg is part of seqan, so the naming is fine.
241 create_directory(tmp_path, err);
242 }
243
244 // .config/seqan cannot be created, try tmp directory.
245 if (err)
246 tmp_path = temp_directory_path(); // choose temp dir instead
247
248 // check if files can be written inside dir
249 path dummy = tmp_path / "dummy.txt";
250 std::ofstream file{dummy};
251 sharg::detail::safe_filesystem_entry file_guard{dummy};
252
253 bool is_open = file.is_open();
254 bool is_good = file.good();
255 file.close();
256 file_guard.remove_no_throw();
257
258 if (!is_good || !is_open) // no write permissions
259 {
260 tmp_path.clear(); // empty path signals no available directory to write to, version check will not be done
261 }
262
263 return tmp_path;
264 }
265
292 {
293 if (developer_approval == update_notifications::off)
294 return false;
295
296 if (std::getenv("SHARG_NO_VERSION_CHECK") != nullptr) // environment variable was set
297 return false;
298
299 if (user_approval.has_value())
300 return user_approval.value();
301
302 // version check was not explicitly handled so let's check the cookie
304 {
305 std::ifstream timestamp_file{timestamp_filename};
306 std::string cookie_line{};
307
308 if (timestamp_file.is_open())
309 {
310 std::getline(timestamp_file, cookie_line); // first line contains the timestamp
311
312 if (get_time_diff_to_current(cookie_line) < 86400 /*one day in seconds*/)
313 {
314 return false;
315 }
316
317 std::getline(timestamp_file, cookie_line); // second line contains the last user decision
318
319 if (cookie_line == "NEVER")
320 {
321 return false;
322 }
323 else if (cookie_line == "ALWAYS")
324 {
325 return true;
326 }
327 // else we do not return but continue to ask the user
328
329 timestamp_file.close();
330 }
331 }
332
333 // Up until now, the user did not specify the --version-check option, the environment variable was not set,
334 // nor did the the cookie tell us what to do. We will now ask the user if possible or do the check by default.
335 write_cookie("ASK"); // Ask again next time when we read the cookie, if this is not overwritten.
336
337 if (detail::stdin_is_terminal() && detail::stderr_is_terminal()) // LCOV_EXCL_START
338 {
339 std::cerr << R"(
340#######################################################################
341 Automatic Update Notifications
342#######################################################################
343
344 This app can look for updates automatically in the background,
345 do you want to do that?
346
347 [a] Always perform version checks for this app.
348 [n] Never perform version checks for this app.
349 [y] Yes, perform a version check now, and ask again tomorrow.
350 [s] Skip the version check now, but ask again tomorrow (the default).
351
352 Please enter one of [a, n, y, s] and press [RETURN].
353
354 For more information, see:
355 https://docs.seqan.de/sharg/main_user/about_update_notifications.html
356
357#######################################################################
358
359)";
360 std::string line{};
361 std::getline(std::cin, line);
362 line.resize(1, 's'); // ignore everything but the first char or resizes the empty string to the default
363
364 switch (line[0])
365 {
366 case 'y':
367 {
368 return true;
369 }
370 case 'a':
371 {
372 write_cookie(std::string{"ALWAYS"}); // overwrite cookie
373 return true;
374 }
375 case 'n':
376 {
377 write_cookie(std::string{"NEVER"}); // overwrite cookie
378 return false;
379 }
380 default:
382 return false;
383 }
384 }
385 }
386 else // of: if (detail::stdin_is_terminal() && detail::stderr_is_terminal())
388 return false; // default: do not check version today, if you cannot ask the user
389 }
390 } // LCOV_EXCL_STOP
391
393 static constexpr std::string_view unregistered_app = "UNREGISTERED_APP";
395 static constexpr std::string_view message_sharg_update =
396 "[SHARG VERSION INFO] :: A new Sharg version is available online.\n"
397 "[SHARG VERSION INFO] :: Please visit www.github.com/seqan/sharg-parser.git for an update\n"
398 "[SHARG VERSION INFO] :: or inform the developer of this app.\n"
399 "[SHARG VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
402 "[SHARG VERSION INFO] :: Thank you for using Sharg!\n"
403 "[SHARG VERSION INFO] :: Do you wish to register your app for update notifications?\n"
404 "[SHARG VERSION INFO] :: Just send an email to support@seqan.de with your app name and version number.\n"
405 "[SHARG VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
408 "[APP VERSION INFO] :: We noticed the app version you use is newer than the one registered with us.\n"
409 "[APP VERSION INFO] :: Please send us an email with the new version so we can correct it "
410 "(support@seqan.de)\n\n";
413 "[APP VERSION INFO] :: A new version of this application is now available.\n"
414 "[APP VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
415 /*Will be extended if a url is given on construction of version_check.*/
418 static constexpr char const * home_env_name
419 {
420#if defined(_WIN32)
421 "UserProfile"
422#else
423 "HOME"
424#endif
425 };
426
430 std::string version{"0.0.0"};
432 std::regex version_regex{"^[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+$"};
437
438private:
440 static std::string get_program()
441 {
442#if defined(_WIN32)
443 return "powershell.exe -NoLogo -NonInteractive -Command \"& {Invoke-WebRequest -erroraction 'silentlycontinue' "
444 "-OutFile";
445#else // Unix based platforms.
446 if (!system("/usr/bin/env -i wget --version > /dev/null 2>&1"))
447 return "/usr/bin/env -i wget --timeout=10 --tries=1 -q -O";
448 else if (!system("/usr/bin/env -i curl --version > /dev/null 2>&1"))
449 return "/usr/bin/env -i curl --connect-timeout 10 -o";
450// In case neither wget nor curl is available try ftp/fetch if system is OpenBSD/FreeBSD.
451// Note, both systems have ftp/fetch command installed by default so we do not guard against it.
452# if defined(__OpenBSD__)
453 return "/usr/bin/env -i ftp -w10 -Vo";
454# elif defined(__FreeBSD__)
455 return "/usr/bin/env -i fetch --timeout=10 -o";
456# else
457 return "";
458# endif // __OpenBSD__
459#endif // defined(_WIN32)
460 }
461
463 double get_time_diff_to_current(std::string const & str_time) const
464 {
465 namespace co = std::chrono;
466 double curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
467
468 double d_time{};
469 std::from_chars(str_time.data(), str_time.data() + str_time.size(), d_time);
470
471 return curr - d_time;
472 }
473
478 {
479 std::array<int, 3> result{};
480
482 return result;
483
484 auto res = std::from_chars(str.data(), str.data() + str.size(), result[0]); // stops and sets res.ptr at '.'
485 res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[1]);
486 res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[2]);
487
488 return result;
489 }
490
495 template <typename msg_type>
496 void write_cookie(msg_type && msg)
497 {
498 // The current time
499 namespace co = std::chrono;
500 auto curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
501
502 std::ofstream timestamp_file{timestamp_filename};
503
504 if (timestamp_file.is_open())
505 {
506 timestamp_file << curr << '\n' << msg;
507 timestamp_file.close();
508 }
509 }
510};
511
512} // 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 safe guard to manage a filesystem entry, e.g. a file or a directory.
Definition safe_filesystem_entry.hpp:35
A functor whose operator() performs the server http request and version checks.
Definition version_check.hpp:55
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:375
void write_cookie(msg_type &&msg)
Writes a cookie file with a specified message.
Definition version_check.hpp:476
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:73
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:381
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:410
std::filesystem::path timestamp_filename
The timestamp filename.
Definition version_check.hpp:416
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:373
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:223
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:420
std::string name
The application name.
Definition version_check.hpp:408
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:387
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:414
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:443
void operator()(std::promise< bool > prom)
Initialises the version_checker with the application name and version.
Definition version_check.hpp:126
std::regex version_regex
The regex to verify a valid version string.
Definition version_check.hpp:412
~version_checker()=default
Defaulted.
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:392
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:457
static constexpr char const * home_env_name
The environment name of the home environment used by getenv()
Definition version_check.hpp:399
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:291
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
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:38
T has_value(T... args)
T pop_back(T... args)
T regex_match(T... args)
T regex_search(T... args)
Provides sharg::detail::safe_filesystem_entry.
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 value(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