SeqAn3 3.4.0-rc.1
The Modern C++ library for sequence analysis.
Loading...
Searching...
No Matches
version_check.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2006-2024 Knut Reinert & Freie Universität Berlin
2// SPDX-FileCopyrightText: 2016-2024 Knut Reinert & MPI für molekulare Genetik
3// SPDX-License-Identifier: BSD-3-Clause
4
10#pragma once
11
12#include <array>
13#include <chrono>
14#include <fstream>
15#include <future>
16#include <iostream>
17#include <optional>
18#include <regex>
19#include <seqan3/std/charconv>
20
25#include <seqan3/version.hpp>
26
27#include <sys/stat.h>
28
29namespace seqan3::detail
30{
31
32// ---------------------------------------------------------------------------------------------------------------------
33// function call_server()
34// ---------------------------------------------------------------------------------------------------------------------
35
42inline 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
56class version_checker
57{
58public:
63 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 " " + out_file.string() + " "
193 + std::string{"http://seqan-update.informatik.uni-tuebingen.de/check/SeqAn3_"} +
194#ifdef __linux
195 "Linux" +
196#elif __APPLE__
197 "MacOS" +
198#elif defined(_WIN32)
199 "Windows" +
200#elif __FreeBSD__
201 "FreeBSD" +
202#elif __OpenBSD__
203 "OpenBSD" +
204#else
205 "unknown" +
206#endif
207#if __x86_64__ || __ppc64__
208 "_64_" +
209#else
210 "_32_" +
211#endif
212 name + // !user input! escaped on construction of the argument parser
213 "_" + version + // !user input! escaped on construction of the version_checker
214#if defined(_WIN32)
215 "; exit [int] -not $?}\" > nul 2>&1";
216#else
217 " > /dev/null 2>&1";
218#endif
219
220 // launch a separate thread to not defer runtime.
221 std::thread(call_server, command, std::move(prom)).detach();
222 }
223
225 static std::filesystem::path get_path()
226 {
227 using namespace std::filesystem;
228
229 path tmp_path;
230
231 tmp_path = std::string{getenv(home_env_name)};
232 tmp_path /= ".config";
233
234 // First, create .config if it does not already exist.
235 std::error_code err;
236 create_directory(tmp_path, err);
237
238 // If this did not fail we, create the seqan subdirectory.
239 if (!err)
240 {
241 tmp_path /= "seqan";
242 create_directory(tmp_path, err);
243 }
244
245 // .config/seqan cannot be created, try tmp directory.
246 if (err)
247 tmp_path = temp_directory_path(); // choose temp dir instead
248
249 // check if files can be written inside dir
250 path dummy = tmp_path / "dummy.txt";
251 std::ofstream file{dummy};
252 detail::safe_filesystem_entry file_guard{dummy};
253
254 bool is_open = file.is_open();
255 bool is_good = file.good();
256 file.close();
257 file_guard.remove_no_throw();
258
259 if (!is_good || !is_open) // no write permissions
260 {
261 tmp_path.clear(); // empty path signals no available directory to write to, version check will not be done
262 }
263
264 return tmp_path;
265 }
266
292 bool decide_if_check_is_performed(update_notifications developer_approval, std::optional<bool> user_approval)
293 {
294 if (developer_approval == update_notifications::off)
295 return false;
296
297 if (std::getenv("SEQAN3_NO_VERSION_CHECK") != nullptr) // environment variable was set
298 return false;
299
300 if (user_approval.has_value())
301 return user_approval.value();
302
303 // version check was not explicitly handled so let's check the cookie
304 if (std::filesystem::exists(cookie_path))
305 {
306 std::ifstream timestamp_file{timestamp_filename};
307 std::string cookie_line{};
308
309 if (timestamp_file.is_open())
310 {
311 std::getline(timestamp_file, cookie_line); // first line contains the timestamp
312
313 if (get_time_diff_to_current(cookie_line) < 86400 /*one day in seconds*/)
314 {
315 return false;
316 }
317
318 std::getline(timestamp_file, cookie_line); // second line contains the last user decision
319
320 if (cookie_line == "NEVER")
321 {
322 return false;
323 }
324 else if (cookie_line == "ALWAYS")
325 {
326 return true;
327 }
328 // else we do not return but continue to ask the user
329
330 timestamp_file.close();
331 }
332 }
333
334 // Up until now, the user did not specify the --version-check option, the environment variable was not set,
335 // nor did the the cookie tell us what to do. We will now ask the user if possible or do the check by default.
336 write_cookie("ASK"); // Ask again next time when we read the cookie, if this is not overwritten.
337
338 if (detail::is_terminal()) // LCOV_EXCL_START
339 {
340 std::cerr << R"(
341#######################################################################
342 Automatic Update Notifications
343#######################################################################
344
345 This app can look for updates automatically in the background,
346 do you want to do that?
347
348 [a] Always perform version checks for this app (the default).
349 [n] Never perform version checks for this app.
350 [y] Yes, perform a version check now, and ask again tomorrow.
351 [s] Skip the version check now, but ask again tomorrow.
352
353 Please enter one of [a, n, y, s] and press [RETURN].
354
355 For more information, see:
356 https://github.com/seqan/seqan3/wiki/Update-Notifications
357
358#######################################################################
359
360)";
361 std::string line{};
362 std::getline(std::cin, line);
363 line.resize(1); // ignore everything but the first char or resizes the empty string to the default
364
365 switch (line[0])
366 {
367 case 'y':
368 {
369 return true;
370 }
371 case 's':
372 {
373 return false;
374 }
375 case 'n':
376 {
377 write_cookie(std::string{"NEVER"}); // overwrite cookie
378 return false;
379 }
380 default:
381 {
382 write_cookie(std::string{"ALWAYS"}); // overwrite cookie
383 return true;
384 }
385 }
386 }
387 else // if !detail::is_terminal()
388 {
389 std::cerr << R"(
390#######################################################################
391 Automatic Update Notifications
392#######################################################################
393 This app performs automatic checks for updates. For more information
394 see: https://github.com/seqan/seqan3/wiki/Update-Notifications
395#######################################################################
396
397)";
398 return true; // default: check version if you cannot ask the user
399 }
400 } // LCOV_EXCL_STOP
401
403 static constexpr std::string_view unregistered_app = "UNREGISTERED_APP";
405 static constexpr std::string_view message_seqan3_update =
406 "[SEQAN3 INFO] :: A new SeqAn version is available online.\n"
407 "[SEQAN3 INFO] :: Please visit www.github.com/seqan/seqan3.git for an update\n"
408 "[SEQAN3 INFO] :: or inform the developer of this app.\n"
409 "[SEQAN3 INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
411 static constexpr std::string_view message_unregistered_app =
412 "[SEQAN3 INFO] :: Thank you for using SeqAn!\n"
413 "[SEQAN3 INFO] :: Do you wish to register your app for update notifications?\n"
414 "[SEQAN3 INFO] :: Just send an email to support@seqan.de with your app name and version number.\n"
415 "[SEQAN3 INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
417 static constexpr std::string_view message_registered_app_update =
418 "[APP INFO] :: We noticed the app version you use is newer than the one registered with us.\n"
419 "[APP INFO] :: Please send us an email with the new version so we can correct it (support@seqan.de)\n\n";
421 std::string message_app_update =
422 "[APP INFO] :: A new version of this application is now available.\n"
423 "[APP INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
424 /*Might be extended if a url is given on construction.*/
425
427 static constexpr char const * home_env_name{
428#if defined(_WIN32)
429 "UserProfile"
430#else
431 "HOME"
432#endif
433 };
434
436 std::string name;
438 std::string version{"0.0.0"};
440 std::regex version_regex{"^[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+$"};
442 std::filesystem::path cookie_path = get_path();
444 std::filesystem::path timestamp_filename;
445
446private:
448 static std::string get_program()
449 {
450#if defined(_WIN32)
451 return "powershell.exe -NoLogo -NonInteractive -Command \"& {Invoke-WebRequest -erroraction 'silentlycontinue' "
452 "-OutFile";
453#else // Unix based platforms.
454 if (!system("/usr/bin/env -i wget --version > /dev/null 2>&1"))
455 return "/usr/bin/env -i wget --timeout=10 --tries=1 -q -O";
456 else if (!system("/usr/bin/env -i curl --version > /dev/null 2>&1"))
457 return "/usr/bin/env -i curl --connect-timeout 10 -o";
458// In case neither wget nor curl is available try ftp/fetch if system is OpenBSD/FreeBSD.
459// Note, both systems have ftp/fetch command installed by default so we do not guard against it.
460# if defined(__OpenBSD__)
461 return "/usr/bin/env -i ftp -w10 -Vo";
462# elif defined(__FreeBSD__)
463 return "/usr/bin/env -i fetch --timeout=10 -o";
464# else
465 return "";
466# endif // __OpenBSD__
467#endif // defined(_WIN32)
468 }
469
471 double get_time_diff_to_current(std::string const & str_time) const
472 {
473 namespace co = std::chrono;
474 double curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
475
476 double d_time{};
477 std::from_chars(str_time.data(), str_time.data() + str_time.size(), d_time);
478
479 return curr - d_time;
480 }
481
485 std::array<int, 3> get_numbers_from_version_string(std::string const & str) const
486 {
487 std::array<int, 3> result{};
488
489 if (!std::regex_match(str, version_regex))
490 return result;
491
492 auto res = std::from_chars(str.data(), str.data() + str.size(), result[0]); // stops and sets res.ptr at '.'
493 res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[1]);
494 res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[2]);
495
496 return result;
497 }
498
503 template <typename msg_type>
504 void write_cookie(msg_type && msg)
505 {
506 // The current time
507 namespace co = std::chrono;
508 auto curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
509
510 std::ofstream timestamp_file{timestamp_filename};
511
512 if (timestamp_file.is_open())
513 {
514 timestamp_file << curr << '\n' << msg;
515 timestamp_file.close();
516 }
517 }
518};
519
520} // 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.
T move(T... args)
update_notifications
Indicates whether application allows automatic update notifications by the seqan3::argument_parser.
Definition auxiliary.hpp:266
@ off
Automatic update notifications should be disabled.
SeqAn specific customisations in the standard namespace.
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:16
#define SEQAN3_VERSION_PATCH
The patch version as MACRO.
Definition version.hpp:20
#define SEQAN3_VERSION_MINOR
The minor version as MACRO.
Definition version.hpp:18
Hide me