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