Sharg 1.1.2-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-2024, Knut Reinert & Freie Universität Berlin
2// SPDX-FileCopyrightText: 2016-2024, Knut Reinert & MPI für molekulare Genetik
3// SPDX-License-Identifier: BSD-3-Clause
4
10#pragma once
11
12#include <array>
13#include <fstream>
14#include <future>
15#include <iostream>
16#include <optional>
17#include <regex>
18#include <sharg/std/charconv>
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.informatik.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 tmp_path = std::string{getenv(home_env_name)};
230 tmp_path /= ".config";
231
232 // First, create .config if it does not already exist.
233 std::error_code err;
234 create_directory(tmp_path, err);
235
236 // If this did not fail we, create the seqan subdirectory.
237 if (!err)
238 {
239 tmp_path /= "seqan"; // sharg is part of seqan, so the naming is fine.
240 create_directory(tmp_path, err);
241 }
242
243 // .config/seqan cannot be created, try tmp directory.
244 if (err)
245 tmp_path = temp_directory_path(); // choose temp dir instead
246
247 // check if files can be written inside dir
248 path dummy = tmp_path / "dummy.txt";
249 std::ofstream file{dummy};
250 sharg::detail::safe_filesystem_entry file_guard{dummy};
251
252 bool is_open = file.is_open();
253 bool is_good = file.good();
254 file.close();
255 file_guard.remove_no_throw();
256
257 if (!is_good || !is_open) // no write permissions
258 {
259 tmp_path.clear(); // empty path signals no available directory to write to, version check will not be done
260 }
261
262 return tmp_path;
263 }
264
291 {
292 if (developer_approval == update_notifications::off)
293 return false;
294
295 if (std::getenv("SHARG_NO_VERSION_CHECK") != nullptr) // environment variable was set
296 return false;
297
298 if (user_approval.has_value())
299 return user_approval.value();
300
301 // version check was not explicitly handled so let's check the cookie
303 {
304 std::ifstream timestamp_file{timestamp_filename};
305 std::string cookie_line{};
306
307 if (timestamp_file.is_open())
308 {
309 std::getline(timestamp_file, cookie_line); // first line contains the timestamp
310
311 if (get_time_diff_to_current(cookie_line) < 86400 /*one day in seconds*/)
312 {
313 return false;
314 }
315
316 std::getline(timestamp_file, cookie_line); // second line contains the last user decision
317
318 if (cookie_line == "NEVER")
319 {
320 return false;
321 }
322 else if (cookie_line == "ALWAYS")
323 {
324 return true;
325 }
326 // else we do not return but continue to ask the user
327
328 timestamp_file.close();
329 }
330 }
331
332 // Up until now, the user did not specify the --version-check option, the environment variable was not set,
333 // nor did the the cookie tell us what to do. We will now ask the user if possible or do the check by default.
334 write_cookie("ASK"); // Ask again next time when we read the cookie, if this is not overwritten.
335
336 if (detail::stdin_is_terminal() && detail::stderr_is_terminal()) // LCOV_EXCL_START
337 {
338 std::cerr << R"(
339#######################################################################
340 Automatic Update Notifications
341#######################################################################
342
343 This app can look for updates automatically in the background,
344 do you want to do that?
345
346 [a] Always perform version checks for this app (the default).
347 [n] Never perform version checks for this app.
348 [y] Yes, perform a version check now, and ask again tomorrow.
349 [s] Skip the version check now, but ask again tomorrow.
350
351 Please enter one of [a, n, y, s] and press [RETURN].
352
353 For more information, see:
354 https://docs.seqan.de/sharg/main_user/about_update_notifications.html
355
356#######################################################################
357
358)";
359 std::string line{};
360 std::getline(std::cin, line);
361 line.resize(1, 's'); // ignore everything but the first char or resizes the empty string to the default
362
363 switch (line[0])
364 {
365 case 'y':
366 {
367 return true;
368 }
369 case 'a':
370 {
371 write_cookie(std::string{"ALWAYS"}); // overwrite cookie
372 return true;
373 }
374 case 'n':
375 {
376 write_cookie(std::string{"NEVER"}); // overwrite cookie
377 return false;
378 }
379 default:
381 return false;
382 }
383 }
384 }
385 else // of: if (detail::stdin_is_terminal() && detail::stderr_is_terminal())
387 return false; // default: do not check version today, if you cannot ask the user
388 }
389 } // LCOV_EXCL_STOP
390
392 static constexpr std::string_view unregistered_app = "UNREGISTERED_APP";
394 static constexpr std::string_view message_sharg_update =
395 "[SHARG VERSION INFO] :: A new Sharg version is available online.\n"
396 "[SHARG VERSION INFO] :: Please visit www.github.com/seqan/sharg-parser.git for an update\n"
397 "[SHARG VERSION INFO] :: or inform the developer of this app.\n"
398 "[SHARG VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
401 "[SHARG VERSION INFO] :: Thank you for using Sharg!\n"
402 "[SHARG VERSION INFO] :: Do you wish to register your app for update notifications?\n"
403 "[SHARG VERSION INFO] :: Just send an email to support@seqan.de with your app name and version number.\n"
404 "[SHARG VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
407 "[APP VERSION INFO] :: We noticed the app version you use is newer than the one registered with us.\n"
408 "[APP VERSION INFO] :: Please send us an email with the new version so we can correct it "
409 "(support@seqan.de)\n\n";
412 "[APP VERSION INFO] :: A new version of this application is now available.\n"
413 "[APP VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
414 /*Will be extended if a url is given on construction of version_check.*/
417 static constexpr char const * home_env_name
418 {
419#if defined(_WIN32)
420 "UserProfile"
421#else
422 "HOME"
423#endif
424 };
425
429 std::string version{"0.0.0"};
431 std::regex version_regex{"^[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+$"};
436
437private:
439 static std::string get_program()
440 {
441#if defined(_WIN32)
442 return "powershell.exe -NoLogo -NonInteractive -Command \"& {Invoke-WebRequest -erroraction 'silentlycontinue' "
443 "-OutFile";
444#else // Unix based platforms.
445 if (!system("/usr/bin/env -i wget --version > /dev/null 2>&1"))
446 return "/usr/bin/env -i wget --timeout=10 --tries=1 -q -O";
447 else if (!system("/usr/bin/env -i curl --version > /dev/null 2>&1"))
448 return "/usr/bin/env -i curl --connect-timeout 10 -o";
449// In case neither wget nor curl is available try ftp/fetch if system is OpenBSD/FreeBSD.
450// Note, both systems have ftp/fetch command installed by default so we do not guard against it.
451# if defined(__OpenBSD__)
452 return "/usr/bin/env -i ftp -w10 -Vo";
453# elif defined(__FreeBSD__)
454 return "/usr/bin/env -i fetch --timeout=10 -o";
455# else
456 return "";
457# endif // __OpenBSD__
458#endif // defined(_WIN32)
459 }
460
462 double get_time_diff_to_current(std::string const & str_time) const
463 {
464 namespace co = std::chrono;
465 double curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
466
467 double d_time{};
468 std::from_chars(str_time.data(), str_time.data() + str_time.size(), d_time);
469
470 return curr - d_time;
471 }
472
477 {
478 std::array<int, 3> result{};
479
481 return result;
482
483 auto res = std::from_chars(str.data(), str.data() + str.size(), result[0]); // stops and sets res.ptr at '.'
484 res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[1]);
485 res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[2]);
486
487 return result;
488 }
489
494 template <typename msg_type>
495 void write_cookie(msg_type && msg)
496 {
497 // The current time
498 namespace co = std::chrono;
499 auto curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
500
501 std::ofstream timestamp_file{timestamp_filename};
502
503 if (timestamp_file.is_open())
504 {
505 timestamp_file << curr << '\n' << msg;
506 timestamp_file.close();
507 }
508 }
509};
510
511} // 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:374
void write_cookie(msg_type &&msg)
Writes a cookie file with a specified message.
Definition version_check.hpp:475
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:380
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:409
std::filesystem::path timestamp_filename
The timestamp filename.
Definition version_check.hpp:415
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:372
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:419
std::string name
The application name.
Definition version_check.hpp:407
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:386
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:413
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:442
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:411
~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:391
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:456
static constexpr char const * home_env_name
The environment name of the home environment used by getenv()
Definition version_check.hpp:398
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:290
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:26
@ 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