SeqAn3 3.2.0
The Modern C++ library for sequence analysis.
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
version_check.hpp
Go to the documentation of this file.
1// -----------------------------------------------------------------------------------------------------
2// Copyright (c) 2006-2022, Knut Reinert & Freie Universität Berlin
3// Copyright (c) 2016-2022, 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 <array>
16#include <chrono>
17#include <fstream>
18#include <future>
19#include <iostream>
20#include <optional>
21#include <regex>
22#include <seqan3/std/charconv>
23
28#include <seqan3/version.hpp>
29
30#include <sys/stat.h>
31
32namespace seqan3::detail
33{
34
35// ---------------------------------------------------------------------------------------------------------------------
36// function call_server()
37// ---------------------------------------------------------------------------------------------------------------------
38
45inline void call_server(std::string const & command, std::promise<bool> prom)
46{
47 // system call - http response is stored in a file '.config/seqan/{appname}_version'
48 if (system(command.c_str()))
49 prom.set_value(false);
50 else
51 prom.set_value(true);
52}
53
54// ---------------------------------------------------------------------------------------------------------------------
55// version_checker
56// ---------------------------------------------------------------------------------------------------------------------
57
59class version_checker
60{
61public:
66 version_checker() = delete;
67 version_checker(version_checker const &) = default;
68 version_checker & operator=(version_checker const &) = default;
69 version_checker(version_checker &&) = default;
70 version_checker & operator=(version_checker &&) = default;
71 ~version_checker() = default;
72
78 version_checker(std::string name_, std::string const & version_, std::string const & app_url = std::string{}) :
79 name{std::move(name_)}
80 {
81 assert(std::regex_match(name, std::regex{"^[a-zA-Z0-9_-]+$"})); // check on construction of the argument parser
82
83 if (!app_url.empty())
84 {
85 message_app_update.pop_back(); // remove second newline
86 message_app_update.append("[APP INFO] :: Visit " + app_url + " for updates.\n\n");
87 }
88
89#if defined(NDEBUG)
90 timestamp_filename = cookie_path / (name + "_usr.timestamp");
91#else
92 timestamp_filename = cookie_path / (name + "_dev.timestamp");
93#endif
94 std::smatch versionMatch;
95
96 // Ensure version string is not corrupt
97 if (!version_.empty() && /*regex allows version prefix instead of exact match */
98 std::regex_search(version_, versionMatch, std::regex("^([[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+).*")))
99 {
100 version = versionMatch.str(1); // in case the git revision number is given take only version number
101 }
102 }
104
131 void operator()(std::promise<bool> prom)
132 {
133 std::array<int, 3> empty_version{0, 0, 0};
134 std::array<int, 3> srv_app_version{};
135 std::array<int, 3> srv_seqan_version{};
136
137 std::ifstream version_file{cookie_path / (name + ".version")};
138
139 if (version_file.is_open())
140 {
141 std::string line{};
142 std::getline(version_file, line); // get first line which should only contain the version number of the app
143
144 if (line != unregistered_app)
145 srv_app_version = get_numbers_from_version_string(line);
146#if !defined(NDEBUG)
147 else
148 std::cerr << message_unregistered_app;
149#endif // !defined(NDEBUG)
150
151 std::getline(version_file, line); // get second line which should only contain the version number of seqan
152 srv_seqan_version = get_numbers_from_version_string(line);
153
154 version_file.close();
155 }
156
157#if !defined(NDEBUG) // only check seqan version in debug
158 if (srv_seqan_version != empty_version)
159 {
161
162 if (seqan_version < srv_seqan_version)
163 std::cerr << message_seqan3_update;
164 }
165#endif
166
167 if (srv_app_version != empty_version) // app version
168 {
169#if defined(NDEBUG) // only check app version in release
170 if (get_numbers_from_version_string(version) < srv_app_version)
171 std::cerr << message_app_update;
172#endif // defined(NDEBUG)
173
174#if !defined(NDEBUG) // only notify developer that app version should be updated on server
175 if (get_numbers_from_version_string(version) > srv_app_version)
176 std::cerr << message_registered_app_update;
177#endif // !defined(NDEBUG)
178 }
179
181
182 std::string program = get_program();
183
184 if (program.empty())
185 {
186 prom.set_value(false);
187 return;
188 }
189
190 // 'cookie_path' is no user input and `name` is escaped on construction of the argument parser.
191 std::filesystem::path out_file = cookie_path / (name + ".version");
192
193 // build up command for server call
194 std::string command = program + // no user defined input
195 " " + out_file.string() + " "
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 "_" + 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 tmp_path = std::string{getenv(home_env_name)};
235 tmp_path /= ".config";
236
237 // First, create .config if it does not already exist.
238 std::error_code err;
239 create_directory(tmp_path, err);
240
241 // If this did not fail we, create the seqan subdirectory.
242 if (!err)
243 {
244 tmp_path /= "seqan";
245 create_directory(tmp_path, err);
246 }
247
248 // .config/seqan cannot be created, try tmp directory.
249 if (err)
250 tmp_path = temp_directory_path(); // choose temp dir instead
251
252 // check if files can be written inside dir
253 path dummy = tmp_path / "dummy.txt";
254 std::ofstream file{dummy};
255 detail::safe_filesystem_entry file_guard{dummy};
256
257 bool is_open = file.is_open();
258 bool is_good = file.good();
259 file.close();
260 file_guard.remove_no_throw();
261
262 if (!is_good || !is_open) // no write permissions
263 {
264 tmp_path.clear(); // empty path signals no available directory to write to, version check will not be done
265 }
266
267 return tmp_path;
268 }
269
295 bool decide_if_check_is_performed(update_notifications developer_approval, std::optional<bool> user_approval)
296 {
297 if (developer_approval == update_notifications::off)
298 return false;
299
300 if (std::getenv("SEQAN3_NO_VERSION_CHECK") != nullptr) // environment variable was set
301 return false;
302
303 if (user_approval.has_value())
304 return user_approval.value();
305
306 // version check was not explicitly handled so let's check the cookie
307 if (std::filesystem::exists(cookie_path))
308 {
309 std::ifstream timestamp_file{timestamp_filename};
310 std::string cookie_line{};
311
312 if (timestamp_file.is_open())
313 {
314 std::getline(timestamp_file, cookie_line); // first line contains the timestamp
315
316 if (get_time_diff_to_current(cookie_line) < 86400 /*one day in seconds*/)
317 {
318 return false;
319 }
320
321 std::getline(timestamp_file, cookie_line); // second line contains the last user decision
322
323 if (cookie_line == "NEVER")
324 {
325 return false;
326 }
327 else if (cookie_line == "ALWAYS")
328 {
329 return true;
330 }
331 // else we do not return but continue to ask the user
332
333 timestamp_file.close();
334 }
335 }
336
337 // Up until now, the user did not specify the --version-check option, the environment variable was not set,
338 // nor did the the cookie tell us what to do. We will now ask the user if possible or do the check by default.
339 write_cookie("ASK"); // Ask again next time when we read the cookie, if this is not overwritten.
340
341 if (detail::is_terminal()) // LCOV_EXCL_START
342 {
343 std::cerr << R"(
344#######################################################################
345 Automatic Update Notifications
346#######################################################################
347
348 This app can look for updates automatically in the background,
349 do you want to do that?
350
351 [a] Always perform version checks for this app (the default).
352 [n] Never perform version checks for this app.
353 [y] Yes, perform a version check now, and ask again tomorrow.
354 [s] Skip the version check now, but ask again tomorrow.
355
356 Please enter one of [a, n, y, s] and press [RETURN].
357
358 For more information, see:
359 https://github.com/seqan/seqan3/wiki/Update-Notifications
360
361#######################################################################
362
363)";
364 std::string line{};
365 std::getline(std::cin, line);
366 line.resize(1); // ignore everything but the first char or resizes the empty string to the default
367
368 switch (line[0])
369 {
370 case 'y':
371 {
372 return true;
373 }
374 case 's':
375 {
376 return false;
377 }
378 case 'n':
379 {
380 write_cookie(std::string{"NEVER"}); // overwrite cookie
381 return false;
382 }
383 default:
384 {
385 write_cookie(std::string{"ALWAYS"}); // overwrite cookie
386 return true;
387 }
388 }
389 }
390 else // if !detail::is_terminal()
391 {
392 std::cerr << R"(
393#######################################################################
394 Automatic Update Notifications
395#######################################################################
396 This app performs automatic checks for updates. For more information
397 see: https://github.com/seqan/seqan3/wiki/Update-Notifications
398#######################################################################
399
400)";
401 return true; // default: check version if you cannot ask the user
402 }
403 } // LCOV_EXCL_STOP
404
406 static constexpr std::string_view unregistered_app = "UNREGISTERED_APP";
408 static constexpr std::string_view message_seqan3_update =
409 "[SEQAN3 INFO] :: A new SeqAn version is available online.\n"
410 "[SEQAN3 INFO] :: Please visit www.github.com/seqan/seqan3.git for an update\n"
411 "[SEQAN3 INFO] :: or inform the developer of this app.\n"
412 "[SEQAN3 INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
414 static constexpr std::string_view message_unregistered_app =
415 "[SEQAN3 INFO] :: Thank you for using SeqAn!\n"
416 "[SEQAN3 INFO] :: Do you wish to register your app for update notifications?\n"
417 "[SEQAN3 INFO] :: Just send an email to support@seqan.de with your app name and version number.\n"
418 "[SEQAN3 INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
420 static constexpr std::string_view message_registered_app_update =
421 "[APP INFO] :: We noticed the app version you use is newer than the one registered with us.\n"
422 "[APP INFO] :: Please send us an email with the new version so we can correct it (support@seqan.de)\n\n";
424 std::string message_app_update =
425 "[APP INFO] :: A new version of this application is now available.\n"
426 "[APP INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
427 /*Might be extended if a url is given on construction.*/
428
430 static constexpr char const * home_env_name
431 {
432#if defined(_WIN32)
433 "UserProfile"
434#else
435 "HOME"
436#endif
437 };
438
440 std::string name;
442 std::string version{"0.0.0"};
444 std::regex version_regex{"^[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+$"};
446 std::filesystem::path cookie_path = get_path();
448 std::filesystem::path timestamp_filename;
449
450private:
452 static std::string get_program()
453 {
454#if defined(_WIN32)
455 return "powershell.exe -NoLogo -NonInteractive -Command \"& {Invoke-WebRequest -erroraction 'silentlycontinue' "
456 "-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
489 std::array<int, 3> get_numbers_from_version_string(std::string const & str) const
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::detail
Provides auxiliary information.
T c_str(T... args)
The <charconv> header from C++17's standard library.
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:267
@ 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)
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