SeqAn3  3.0.3
The Modern C++ library for sequence analysis.
version_check.hpp
Go to the documentation of this file.
1 // -----------------------------------------------------------------------------------------------------
2 // Copyright (c) 2006-2020, Knut Reinert & Freie Universität Berlin
3 // Copyright (c) 2016-2020, Knut Reinert & MPI für molekulare Genetik
4 // This file may be used, modified and/or redistributed under the terms of the 3-clause BSD-License
5 // shipped with this file and also available at: https://github.com/seqan/seqan3/blob/master/LICENSE.md
6 // -----------------------------------------------------------------------------------------------------
7 
13 #pragma once
14 
15 #include <sys/stat.h>
16 
17 #include <chrono>
18 #include <fstream>
19 #include <future>
20 #include <iostream>
21 #include <regex>
22 
27 #include <seqan3/std/charconv>
28 #include <seqan3/version.hpp>
29 
30 namespace seqan3::detail
31 {
32 
33 // ---------------------------------------------------------------------------------------------------------------------
34 // function call_server()
35 // ---------------------------------------------------------------------------------------------------------------------
36 
43 inline void call_server(std::string const & command, std::promise<bool> prom)
44 {
45  // system call - http response is stored in a file '.config/seqan/{appname}_version'
46  if (system(command.c_str()))
47  prom.set_value(false);
48  else
49  prom.set_value(true);
50 }
51 
52 // ---------------------------------------------------------------------------------------------------------------------
53 // version_checker
54 // ---------------------------------------------------------------------------------------------------------------------
55 
58 {
59 public:
64  version_checker() = delete;
65  version_checker(version_checker const &) = default;
66  version_checker & operator=(version_checker const &) = default;
69  ~version_checker() = default;
70 
76  version_checker(std::string name_, std::string const & version_, std::string const & app_url = std::string{}) :
77  name{std::move(name_)}
78  {
79  assert(std::regex_match(name, std::regex{"^[a-zA-Z0-9_-]+$"})); // check on construction of the argument parser
80 
81  if (!app_url.empty())
82  {
83  message_app_update.pop_back(); // remove second newline
84  message_app_update.append("[APP INFO] :: Visit " + app_url + " for updates.\n\n");
85  }
86 
87 #if defined(NDEBUG)
88  timestamp_filename = cookie_path / (name + "_usr.timestamp");
89 #else
90  timestamp_filename = cookie_path / (name + "_dev.timestamp");
91 #endif
92  std::smatch versionMatch;
93 
94  // Ensure version string is not corrupt
95  if (!version_.empty() && /*regex allows version prefix instead of exact match */
96  std::regex_search(version_, versionMatch, std::regex("^([[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+).*")))
97  {
98  version = versionMatch.str(1); // in case the git revision number is given take only version number
99  }
100  }
102 
130  {
131  std::array<int, 3> empty_version{0, 0, 0};
132  std::array<int, 3> srv_app_version{};
133  std::array<int, 3> srv_seqan_version{};
134 
135  std::ifstream version_file{cookie_path / (name + ".version")};
136 
137  if (version_file.is_open())
138  {
139  std::string line{};
140  std::getline(version_file, line); // get first line which should only contain the version number of the app
141 
142  if (line != unregistered_app)
143  srv_app_version = get_numbers_from_version_string(line);
144 #if !defined(NDEBUG)
145  else
147 #endif // !defined(NDEBUG)
148 
149  std::getline(version_file, line); // get second line which should only contain the version number of seqan
150  srv_seqan_version = get_numbers_from_version_string(line);
151 
152  version_file.close();
153  }
154 
155 #if !defined(NDEBUG) // only check seqan version in debug
156  if (srv_seqan_version != empty_version)
157  {
159 
160  if (seqan_version < srv_seqan_version)
162  }
163 #endif
164 
165  if (srv_app_version != empty_version) // app version
166  {
167 #if defined(NDEBUG) // only check app version in release
168  if (get_numbers_from_version_string(version) < srv_app_version)
170 #endif // defined(NDEBUG)
171 
172 #if !defined(NDEBUG) // only notify developer that app version should be updated on server
173  if (get_numbers_from_version_string(version) > srv_app_version)
175 #endif // !defined(NDEBUG)
176  }
177 
179 
180  std::string program = get_program();
181 
182  if (program.empty())
183  {
184  prom.set_value(false);
185  return;
186  }
187 
188  // 'cookie_path' is no user input and `name` is escaped on construction of the argument parser.
189  std::filesystem::path out_file = cookie_path / (name + ".version");
190 
191  // build up command for server call
192  std::string command = program + // no user defined input
193  " " +
194  out_file.string() +
195  " " +
196  std::string{"http://seqan-update.informatik.uni-tuebingen.de/check/SeqAn3_"} +
197 #ifdef __linux
198  "Linux" +
199 #elif __APPLE__
200  "MacOS" +
201 #elif defined(_WIN32)
202  "Windows" +
203 #elif __FreeBSD__
204  "FreeBSD" +
205 #elif __OpenBSD__
206  "OpenBSD" +
207 #else
208  "unknown" +
209 #endif
210 #if __x86_64__ || __ppc64__
211  "_64_" +
212 #else
213  "_32_" +
214 #endif
215  name + // !user input! escaped on construction of the argument parser
216  "_" +
217  version + // !user input! escaped on construction of the version_checker
218 #if defined(_WIN32)
219  "; exit [int] -not $?}\" > nul 2>&1";
220 #else
221  " > /dev/null 2>&1";
222 #endif
223 
224  // launch a separate thread to not defer runtime.
225  std::thread(call_server, command, std::move(prom)).detach();
226  }
227 
230  {
231  using namespace std::filesystem;
232 
233  path tmp_path;
234 
235  tmp_path = std::string{getenv(home_env_name)};
236  tmp_path /= ".config";
237 
238  // First, create .config if it does not already exist.
239  std::error_code err;
240  create_directory(tmp_path, err);
241 
242  // If this did not fail we, create the seqan subdirectory.
243  if (!err)
244  {
245  tmp_path /= "seqan";
246  create_directory(tmp_path, err);
247  }
248 
249  // .config/seqan cannot be created, try tmp directory.
250  if (err)
251  tmp_path = temp_directory_path(); // choose temp dir instead
252 
253  // check if files can be written inside dir
254  path dummy = tmp_path / "dummy.txt";
255  std::ofstream file{dummy};
256  detail::safe_filesystem_entry file_guard{dummy};
257 
258  bool is_open = file.is_open();
259  bool is_good = file.good();
260  file.close();
261  file_guard.remove_no_throw();
262 
263  if (!is_good || !is_open) // no write permissions
264  {
265  tmp_path.clear(); // empty path signals no available directory to write to, version check will not be done
266  }
267 
268  return tmp_path;
269  }
270 
297  {
298  if (developer_approval == update_notifications::off)
299  return false;
300 
301  if (std::getenv("SEQAN3_NO_VERSION_CHECK") != nullptr) // environment variable was set
302  return false;
303 
304  if (user_approval.has_value())
305  return user_approval.value();
306 
307  // version check was not explicitly handled so let's check the cookie
309  {
310  std::ifstream timestamp_file{timestamp_filename};
311  std::string cookie_line{};
312 
313  if (timestamp_file.is_open())
314  {
315  std::getline(timestamp_file, cookie_line); // first line contains the timestamp
316 
317  if (get_time_diff_to_current(cookie_line) < 86400/*one day in seconds*/)
318  {
319  return false;
320  }
321 
322  std::getline(timestamp_file, cookie_line); // second line contains the last user decision
323 
324  if (cookie_line == "NEVER")
325  {
326  return false;
327  }
328  else if (cookie_line == "ALWAYS")
329  {
330  return true;
331  }
332  // else we do not return but continue to ask the user
333 
334  timestamp_file.close();
335  }
336  }
337 
338  // Up until now, the user did not specify the --version-check option, the environment variable was not set,
339  // nor did the the cookie tell us what to do. We will now ask the user if possible or do the check by default.
340  write_cookie("ASK"); // Ask again next time when we read the cookie, if this is not overwritten.
341 
342  if (detail::is_terminal()) // LCOV_EXCL_START
343  {
344  std::cerr << R"(
345 #######################################################################
346  Automatic Update Notifications
347 #######################################################################
348 
349  This app can look for updates automatically in the background,
350  do you want to do that?
351 
352  [a] Always perform version checks for this app (the default).
353  [n] Never perform version checks for this app.
354  [y] Yes, perform a version check now, and ask again tomorrow.
355  [s] Skip the version check now, but ask again tomorrow.
356 
357  Please enter one of [a, n, y, s] and press [RETURN].
358 
359  For more information, see:
360  https://github.com/seqan/seqan3/wiki/Update-Notifications
361 
362 #######################################################################
363 
364 )";
365  std::string line{};
366  std::getline(std::cin, line);
367  line.resize(1); // ignore everything but the first char or resizes the empty string to the default
368 
369  switch (line[0])
370  {
371  case 'y':
372  {
373  return true;
374  }
375  case 's':
376  {
377  return false;
378  }
379  case 'n':
380  {
381  write_cookie(std::string{"NEVER"}); // overwrite cookie
382  return false;
383  }
384  default:
385  {
386  write_cookie(std::string{"ALWAYS"}); // overwrite cookie
387  return true;
388  }
389  }
390  }
391  else // if !detail::is_terminal()
392  {
393  std::cerr << R"(
394 #######################################################################
395  Automatic Update Notifications
396 #######################################################################
397  This app performs automatic checks for updates. For more information
398  see: https://github.com/seqan/seqan3/wiki/Update-Notifications
399 #######################################################################
400 
401 )";
402  return true; // default: check version if you cannot ask the user
403  }
404  } // LCOV_EXCL_STOP
405 
407  static constexpr std::string_view unregistered_app = "UNREGISTERED_APP";
409  static constexpr std::string_view message_seqan3_update =
410  "[SEQAN3 INFO] :: A new SeqAn version is available online.\n"
411  "[SEQAN3 INFO] :: Please visit www.github.com/seqan/seqan3.git for an update\n"
412  "[SEQAN3 INFO] :: or inform the developer of this app.\n"
413  "[SEQAN3 INFO] :: If you don't wish to receive further notifications, set --version-check OFF.\n\n";
416  "[SEQAN3 INFO] :: Thank you for using SeqAn!\n"
417  "[SEQAN3 INFO] :: Do you wish to register your app for update notifications?\n"
418  "[SEQAN3 INFO] :: Just send an email to support@seqan.de with your app name and version number.\n"
419  "[SEQAN3 INFO] :: If you don't wish to receive further notifications, set --version-check OFF.\n\n";
422  "[APP INFO] :: We noticed the app version you use is newer than the one registered with us.\n"
423  "[APP INFO] :: Please send us an email with the new version so we can correct it (support@seqan.de)\n\n";
426  "[APP INFO] :: A new version of this application is now available.\n"
427  "[APP INFO] :: If you don't wish to receive further notifications, set --version-check OFF.\n\n";
428  /*Might be extended if a url is given on construction.*/
429 
431  static constexpr char const * home_env_name
432  {
433 #if defined(_WIN32)
434  "UserProfile"
435 #else
436  "HOME"
437 #endif
438  };
439 
443  std::string version{"0.0.0"};
445  std::regex version_regex{"^[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+$"};
450 
451 private:
453  static std::string get_program()
454  {
455 #if defined(_WIN32)
456  return "powershell.exe -NoLogo -NonInteractive -Command \"& {Invoke-WebRequest -erroraction 'silentlycontinue' -OutFile";
457 #else // Unix based platforms.
458  if (!system("/usr/bin/env -i wget --version > /dev/null 2>&1"))
459  return "/usr/bin/env -i wget --timeout=10 --tries=1 -q -O";
460  else if (!system("/usr/bin/env -i curl --version > /dev/null 2>&1"))
461  return "/usr/bin/env -i curl --connect-timeout 10 -o";
462  // In case neither wget nor curl is available try ftp/fetch if system is OpenBSD/FreeBSD.
463  // Note, both systems have ftp/fetch command installed by default so we do not guard against it.
464  #if defined(__OpenBSD__)
465  return "/usr/bin/env -i ftp -w10 -Vo";
466  #elif defined(__FreeBSD__)
467  return "/usr/bin/env -i fetch --timeout=10 -o";
468  #else
469  return "";
470  #endif // __OpenBSD__
471 #endif // defined(_WIN32)
472  }
473 
475  double get_time_diff_to_current(std::string const & str_time) const
476  {
477  namespace co = std::chrono;
478  double curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
479 
480  double d_time{};
481  std::from_chars(str_time.data(), str_time.data() + str_time.size(), d_time);
482 
483  return curr - d_time;
484  }
485 
490  {
491  std::array<int, 3> result{};
492 
493  if (!std::regex_match(str, version_regex))
494  return result;
495 
496  auto res = std::from_chars(str.data(), str.data() + str.size(), result[0]); // stops and sets res.ptr at '.'
497  res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[1]);
498  res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[2]);
499 
500  return result;
501  }
502 
507  template <typename msg_type>
508  void write_cookie(msg_type && msg)
509  {
510  // The current time
511  namespace co = std::chrono;
512  auto curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
513 
514  std::ofstream timestamp_file{timestamp_filename};
515 
516  if (timestamp_file.is_open())
517  {
518  timestamp_file << curr << '\n' << msg;
519  timestamp_file.close();
520  }
521  }
522 };
523 
524 } // namespace seqan3
T append(T... args)
Provides auxiliary information.
T c_str(T... args)
A safe guard to manage a filesystem entry, e.g. a file or a directory.
Definition: safe_filesystem_entry.hpp:38
A functor whose operator() performs the server http request and version checks.
Definition: version_check.hpp:58
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:425
std::filesystem::path timestamp_filename
The timestamp filename.
Definition: version_check.hpp:421
std::filesystem::path cookie_path
The path to store timestamp and version files (either ~/.config/seqan or the tmp directory).
Definition: version_check.hpp:419
static constexpr char const * home_env_name
The environment name of the home environment used by getenv()
Definition: version_check.hpp:404
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:393
std::regex version_regex
The regex to verify a valid version string.
Definition: version_check.hpp:417
void write_cookie(msg_type &&msg)
Writes a cookie file with a specified message.
Definition: version_check.hpp:480
version_checker & operator=(version_checker const &)=default
Defaulted.
std::string version
The version of the application.
Definition: version_check.hpp:415
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:447
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:76
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:229
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:379
~version_checker()=default
Defaulted.
std::string name
The application name.
Definition: version_check.hpp:413
static constexpr std::string_view message_seqan3_update
The message directed to the developer of the app if a new seqan3 version is available.
Definition: version_check.hpp:381
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:296
version_checker(version_checker &&)=default
Defaulted.
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:387
version_checker(version_checker const &)=default
Defaulted.
void operator()(std::promise< bool > prom)
Initialises the version_checker with the application name and version.
Definition: version_check.hpp:129
version_checker()=delete
This class has to be initialised with name and version information.
version_checker & operator=(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:397
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:461
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)
auto const move
A view that turns lvalue-references into rvalue-references.
Definition: move.hpp:70
Provides various utility functions.
The internal SeqAn3 namespace.
Definition: aligned_sequence_concept.hpp:29
bool is_terminal()
Check whether we are printing to a terminal.
Definition: terminal.hpp:38
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:43
update_notifications
Indicates whether application allows automatic update notifications by the seqan3::argument_parser.
Definition: auxiliary.hpp:258
@ off
Automatic update notifications should be disabled.
T has_value(T... args)
T pop_back(T... args)
T regex_match(T... args)
T regex_search(T... args)
Provides seqan3::detail::safe_filesystem_entry.
T set_value(T... args)
T size(T... args)
Provides std::from_chars and std::to_chars if not defined in the stl <charconv> header.
T str(T... args)
Checks if program is run interactively and retrieves dimensions of terminal (Transferred from seqan2)...
T value(T... args)
Provides SeqAn version macros and global variables.
#define SEQAN3_VERSION_MAJOR
The major version as MACRO.
Definition: version.hpp:19
#define SEQAN3_VERSION_PATCH
The patch version as MACRO.
Definition: version.hpp:23
#define SEQAN3_VERSION_MINOR
The minor version as MACRO.
Definition: version.hpp:21