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