SeqAn3 3.1.0
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
31namespace seqan3::detail
32{
33
34// ---------------------------------------------------------------------------------------------------------------------
35// function call_server()
36// ---------------------------------------------------------------------------------------------------------------------
37
44inline 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
58class version_checker
59{
60public:
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 {
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
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 false.\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 false.\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 false.\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
452private:
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.
T c_str(T... args)
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)
Provides various utility functions.
update_notifications
Indicates whether application allows automatic update notifications by the seqan3::argument_parser.
Definition: auxiliary.hpp:268
@ off
Automatic update notifications should be disabled.
T has_value(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)
The <charconv> header from C++17's standard library.
T str(T... args)
T system(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