SeqAn3
The Modern C++ library for sequence analysis.
version_check.hpp
Go to the documentation of this file.
1 // -----------------------------------------------------------------------------------------------------
2 // Copyright (c) 2006-2019, Knut Reinert & Freie Universität Berlin
3 // Copyright (c) 2016-2019, 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
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 
24 
25 namespace seqan3::detail
26 {
27 
28 // ---------------------------------------------------------------------------------------------------------------------
29 // function call_server()
30 // ---------------------------------------------------------------------------------------------------------------------
31 
38 inline 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 
52 class version_checker
53 {
54 public:
58  version_checker() = delete;
60  version_checker(version_checker const &) = default;
61  version_checker & operator=(version_checker const &) = default;
62  version_checker(version_checker &&) = default;
63  version_checker & operator=(version_checker &&) = default;
64  ~version_checker() = default;
65 
71  version_checker(std::string name_, std::string const & version_, std::string const & app_url = std::string{}) :
72  name{std::move(name_)}
73  {
74  if (!app_url.empty())
75  {
76  message_app_update.pop_back(); // remove second newline
77  message_app_update.append("[APP INFO] :: Visit " + app_url + " for updates.\n\n");
78  }
79 
80 #if defined(NDEBUG)
81  timestamp_filename = cookie_path / (name + "_usr.timestamp");
82 #else
83  timestamp_filename = cookie_path / (name + "_dev.timestamp");
84 #endif
85  std::smatch versionMatch;
86 
87  if (!version_.empty() && /*regex allows version prefix instead of exact match */
88  std::regex_search(version_, versionMatch, std::regex("^([[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+).*")))
89  {
90  version = versionMatch.str(1); // in case the git revision number is given take only version number
91  }
92 
93  server_url = std::string{"http://seqan-update.informatik.uni-tuebingen.de/check/SeqAn3_"} +
94  get_os() + get_bit_sys() + name + "_" + version;
95 
96  program = get_program();
97 
98  // build up command for server call
99  if (!program.empty())
100  {
101  command = program + " " + cookie_path.string() + "/" + name + ".version " + server_url;
102 #if defined(_WIN32)
103  command = command + "; exit [int] -not $?}\" > nul 2>&1";
104 #else
105  command = command + " > /dev/null 2>&1";
106 #endif
107  }
108  }
110 
137  void operator()(std::promise<bool> prom)
138  {
139  std::array<int, 3> empty_version{0, 0, 0};
140  std::array<int, 3> srv_app_version{};
141  std::array<int, 3> srv_seqan_version{};
142 
143  std::ifstream version_file{cookie_path / (name + ".version")};
144 
145  if (version_file.is_open())
146  {
147  std::string line{};
148  std::getline(version_file, line); // get first line which should only contain the version number of the app
149 
150  if (line != unregistered_app)
151  srv_app_version = get_numbers_from_version_string(line);
152 #if !defined(NDEBUG)
153  else
154  std::cerr << message_unregistered_app;
155 #endif // !defined(NDEBUG)
156 
157  std::getline(version_file, line); // get second line which should only contain the version number of seqan
158  srv_seqan_version = get_numbers_from_version_string(line);
159 
160  version_file.close();
161  }
162 
163 #if !defined(NDEBUG) // only check seqan version in debug
164  if (srv_seqan_version != empty_version)
165  {
167 
168  if (seqan_version < srv_seqan_version)
169  std::cerr << message_seqan3_update;
170  }
171 #endif
172 
173  if (srv_app_version != empty_version) // app version
174  {
175 #if defined(NDEBUG) // only check app version in release
176  if (get_numbers_from_version_string(version) < srv_app_version)
177  std::cerr << message_app_update;
178 #endif // defined(NDEBUG)
179 
180 #if !defined(NDEBUG) // only notify developer that app version should be updated on server
181  if (get_numbers_from_version_string(version) > srv_app_version)
182  std::cerr << message_registered_app_update;
183 #endif // !defined(NDEBUG)
184  }
185 
187 
188  if (program.empty())
189  {
190  prom.set_value(false);
191  return;
192  }
193 
194  // launch a separate thread to not defer runtime.
195  std::thread(call_server, command, std::move(prom)).detach();
196  }
197 
199  static std::filesystem::path get_path()
200  {
201  using namespace std::filesystem;
202 
203  path tmp_path;
204 
205 #if defined(_WIN32)
206  tmp_path = std::string{getenv("UserProfile")};
207 #else
208  tmp_path = std::string{getenv("HOME")};
209 #endif
210  tmp_path /= ".config";
211  tmp_path /= "seqan";
212 
213  // create directory if it does not already exist
214  std::error_code err;
215  std::filesystem::create_directory(tmp_path, err); // does nothing and err is zero if path already exists.
216 
217  if (err)
218  tmp_path = std::filesystem::temp_directory_path(); // choose temp dir instead
219 
220  // check if files can be written inside dir
221  path dummy = tmp_path / "dummy.txt";
222  std::ofstream file{dummy};
223  detail::safe_filesystem_entry file_guard{dummy};
224 
225  bool is_open = file.is_open();
226  bool is_good = file.good();
227  file.close();
228  file_guard.remove_no_throw();
229 
230  if (!is_good || !is_open) // no write permissions
231  {
232  tmp_path.clear(); // empty path signals no available directory to write to, version check will not be done
233  }
234 
235  return tmp_path;
236  }
237 
262  bool decide_if_check_is_performed(bool developer_approval, std::optional<bool> user_approval)
263  {
264  if (!developer_approval)
265  return false;
266 
267  if (std::getenv("SEQAN3_NO_VERSION_CHECK") != nullptr) // environment variable was set
268  return false;
269 
270  if (user_approval.has_value())
271  return user_approval.value();
272 
273  // version check was not explicitly handled so let's check the cookie
274  if (std::filesystem::exists(cookie_path))
275  {
276  std::ifstream timestamp_file{timestamp_filename};
277  std::string cookie_line{};
278 
279  if (timestamp_file.is_open())
280  {
281  std::getline(timestamp_file, cookie_line); // first line contains the timestamp
282 
283  if (get_time_diff_to_current(cookie_line) < 86400/*one day in seconds*/)
284  {
285  return false;
286  }
287 
288  std::getline(timestamp_file, cookie_line); // second line contains the last user decision
289 
290  if (cookie_line == "NEVER")
291  {
292  return false;
293  }
294  else if (cookie_line == "ALWAYS")
295  {
296  return true;
297  }
298  // else we do not return but continue to ask the user
299 
300  timestamp_file.close();
301  }
302  }
303 
304  // Up until now, the user did not specify the --version-check option, the environment variable was not set,
305  // nor did the the cookie tell us what to do. We will now ask the user if possible or do the check by default.
306  write_cookie("ASK"); // Ask again next time when we read the cookie, if this is not overwritten.
307 
308  if (detail::is_terminal())
309  {
310  std::cerr << R"(
311 #######################################################################
312  Automatic Update Notifications
313 #######################################################################
314 
315  This app can look for updates automatically in the background,
316  do you want to do that?
317 
318  [a] Always perform version checks for this app (the default).
319  [n] Never perform version checks for this app.
320  [y] Yes, perform a version check now, and ask again tomorrow.
321  [s] Skip the version check now, but ask again tomorrow.
322 
323  Please enter one of [a, n, y, s] and press [RETURN].
324 
325  For more information, see:
326  https://github.com/seqan/seqan3/wiki/Update-Notifications
327 
328 #######################################################################
329 
330 )";
331  char chr{};
332  std::cin >> chr;
333 
334  switch (chr)
335  {
336  case 'y':
337  {
338  return true;
339  }
340  case 's':
341  {
342  return false;
343  }
344  case 'n':
345  {
346  write_cookie(std::string{"NEVER"}); // overwrite cookie
347  return false;
348  }
349  default:
350  {
351  write_cookie(std::string{"ALWAYS"}); // overwrite cookie
352  return true;
353  }
354  }
355  }
356  else // if !detail::is_terminal()
357  {
358  std::cerr << R"(
359 #######################################################################
360  Automatic Update Notifications
361 #######################################################################
362  This app performs automatic checks for updates. For more information
363  see: https://github.com/seqan/seqan3/wiki/Update-Notifications
364 #######################################################################
365 
366 )";
367  return true; // default: check version if you cannot ask the user
368  }
369  }
370 
372  static constexpr std::string_view unregistered_app = "UNREGISTERED_APP";
374  static constexpr std::string_view message_seqan3_update =
375  "[SEQAN3 INFO] :: A new SeqAn3 version is available online.\n"
376  "[SEQAN3 INFO] :: Please visit www.github.com/seqan/seqan3.git for an update\n"
377  "[SEQAN3 INFO] :: or inform the developer of this app.\n"
378  "[SEQAN3 INFO] :: If you don't wish to receive further notifications, set --version-check OFF.\n\n";
380  static constexpr std::string_view message_unregistered_app =
381  "[SEQAN3 INFO] :: Thank you for using SeqAn!\n"
382  "[SEQAN3 INFO] :: Do you wish to register your app for update notifications?\n"
383  "[SEQAN3 INFO] :: Just send an email to support@seqan.de with your app name and version number.\n"
384  "[SEQAN3 INFO] :: If you don't wish to receive further notifications, set --version-check OFF.\n\n";
386  static constexpr std::string_view message_registered_app_update =
387  "[APP INFO] :: We noticed the app version you use is newer than the one registered with us.\n"
388  "[APP INFO] :: Please send us an email with the new version so we can correct it (support@seqan.de)\n\n";
390  std::string message_app_update =
391  "[APP INFO] :: A new version of this application is now available.\n"
392  "[APP INFO] :: If you don't wish to receive further notifications, set --version-check OFF.\n\n";
393  /*Might be extended if a url is given on construction.*/
394 
396  std::string server_url;
398  std::string name;
400  std::string version{"0.0.0"};
402  std::regex version_regex{"^[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+$"};
404  std::string program;
406  std::string command;
408  std::filesystem::path cookie_path = get_path();
410  std::filesystem::path timestamp_filename;
411 
412 private:
414  static std::string get_program()
415  {
416 #if defined(_WIN32)
417  return "powershell.exe -NoLogo -NonInteractive -Command \"& {Invoke-WebRequest -erroraction 'silentlycontinue' -OutFile";
418 #else // Unix based platforms.
419  if (!system("wget --version > /dev/null 2>&1"))
420  return "wget -q -O";
421  else if (!system("curl --version > /dev/null 2>&1"))
422  return "curl -o";
423  #ifndef __linux // ftp call does not work on linux
424  else if (!system("which ftp > /dev/null 2>&1"))
425  return "ftp -Vo";
426  #endif // __linux
427  else
428  return "";
429 #endif // defined(_WIN32)
430  }
431 
433  static constexpr const char * get_os() noexcept
434  {
435 #ifdef __linux
436  return "Linux";
437 #elif __APPLE__
438  return "MacOS";
439 #elif defined(_WIN32)
440  return "Windows";
441 #elif __FreeBSD__
442  return "FreeBSD";
443 #elif __OpenBSD__
444  return "OpenBSD";
445 #else
446  return "unknown";
447 #endif
448  }
449 
451  static constexpr const char * get_bit_sys() noexcept
452  {
453 #if __x86_64__ || __ppc64__
454  return "_64_";
455 #else
456  return "_32_";
457 #endif
458  }
459 
461  double get_time_diff_to_current(std::string const & str_time) const
462  {
463  namespace co = std::chrono;
464  double curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
465 
466  double d_time{};
467  std::from_chars(str_time.data(), str_time.data() + str_time.size(), d_time);
468 
469  return curr - d_time;
470  }
471 
475  std::array<int, 3> get_numbers_from_version_string(std::string const & str) const
476  {
477  std::array<int, 3> result{};
478 
479  if (!std::regex_match(str, version_regex))
480  return result;
481 
482  auto res = std::from_chars(str.data(), str.data() + str.size(), result[0]); // stops and sets res.ptr at '.'
483  res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[1]);
484  res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[2]);
485 
486  return result;
487  }
488 
493  template <typename msg_type>
494  void write_cookie(msg_type && msg)
495  {
496  // The current time
497  namespace co = std::chrono;
498  auto curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
499 
500  std::ofstream timestamp_file{timestamp_filename};
501 
502  if (timestamp_file.is_open())
503  {
504  timestamp_file << curr << '\n' << msg;
505  timestamp_file.close();
506  }
507  }
508 };
509 
510 } // namespace seqan3
#define SEQAN3_VERSION_MINOR
The minor version as MACRO.
Definition: version.hpp:22
T exists(T... args)
T empty(T... args)
T value(T... args)
T regex_match(T... args)
T temp_directory_path(T... args)
T system(T... args)
T getline(T... args)
T getenv(T... args)
T data(T... args)
Provides various utility functions.
std::from_chars_result from_chars(char const *first, char const *last, value_type &value, int base) noexcept
Parse a char sequence into an integral.
Definition: charconv:174
T str(T... args)
T count(T... args)
T flush(T... args)
Definition: aligned_sequence_concept.hpp:35
T create_directory(T... args)
T size(T... args)
T c_str(T... args)
T has_value(T... args)
T regex_search(T... args)
T set_value(T... args)
T detach(T... args)
#define SEQAN3_VERSION_PATCH
The patch version as MACRO.
Definition: version.hpp:24
#define SEQAN3_VERSION_MAJOR
The major version as MACRO.
Definition: version.hpp:20