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