Sharg 1.1.0
The argument parser for bio-c++ tools.
 
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Modules Pages Concepts
Loading...
Searching...
No Matches
version_check.hpp
Go to the documentation of this file.
1// --------------------------------------------------------------------------------------------------------
2// Copyright (c) 2006-2023, Knut Reinert & Freie Universität Berlin
3// Copyright (c) 2016-2023, 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::stdin_is_terminal() && detail::stderr_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, 's'); // 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 'a':
373 {
374 write_cookie(std::string{"ALWAYS"}); // overwrite cookie
375 return true;
376 }
377 case 'n':
378 {
379 write_cookie(std::string{"NEVER"}); // overwrite cookie
380 return false;
381 }
382 default:
383 {
384 return false;
385 }
386 }
387 }
388 else // of: if (detail::stdin_is_terminal() && detail::stderr_is_terminal())
389 {
390 return false; // default: do not check version today, if you cannot ask the user
391 }
392 } // LCOV_EXCL_STOP
393
395 static constexpr std::string_view unregistered_app = "UNREGISTERED_APP";
397 static constexpr std::string_view message_sharg_update =
398 "[SHARG VERSION INFO] :: A new Sharg version is available online.\n"
399 "[SHARG VERSION INFO] :: Please visit www.github.com/seqan/sharg-parser.git for an update\n"
400 "[SHARG VERSION INFO] :: or inform the developer of this app.\n"
401 "[SHARG VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
403 static constexpr std::string_view message_unregistered_app =
404 "[SHARG VERSION INFO] :: Thank you for using Sharg!\n"
405 "[SHARG VERSION INFO] :: Do you wish to register your app for update notifications?\n"
406 "[SHARG VERSION INFO] :: Just send an email to support@seqan.de with your app name and version number.\n"
407 "[SHARG VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
409 static constexpr std::string_view message_registered_app_update =
410 "[APP VERSION INFO] :: We noticed the app version you use is newer than the one registered with us.\n"
411 "[APP VERSION INFO] :: Please send us an email with the new version so we can correct it "
412 "(support@seqan.de)\n\n";
414 std::string message_app_update =
415 "[APP VERSION INFO] :: A new version of this application is now available.\n"
416 "[APP VERSION INFO] :: If you don't wish to receive further notifications, set --version-check false.\n\n";
417 /*Will be extended if a url is given on construction of version_check.*/
418
420 static constexpr char const * home_env_name
421 {
422#if defined(_WIN32)
423 "UserProfile"
424#else
425 "HOME"
426#endif
427 };
428
430 std::string name;
432 std::string version{"0.0.0"};
434 std::regex version_regex{"^[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+$"};
436 std::filesystem::path cookie_path = get_path();
438 std::filesystem::path timestamp_filename;
439
440private:
442 static std::string get_program()
443 {
444#if defined(_WIN32)
445 return "powershell.exe -NoLogo -NonInteractive -Command \"& {Invoke-WebRequest -erroraction 'silentlycontinue' "
446 "-OutFile";
447#else // Unix based platforms.
448 if (!system("/usr/bin/env -i wget --version > /dev/null 2>&1"))
449 return "/usr/bin/env -i wget --timeout=10 --tries=1 -q -O";
450 else if (!system("/usr/bin/env -i curl --version > /dev/null 2>&1"))
451 return "/usr/bin/env -i curl --connect-timeout 10 -o";
452// In case neither wget nor curl is available try ftp/fetch if system is OpenBSD/FreeBSD.
453// Note, both systems have ftp/fetch command installed by default so we do not guard against it.
454# if defined(__OpenBSD__)
455 return "/usr/bin/env -i ftp -w10 -Vo";
456# elif defined(__FreeBSD__)
457 return "/usr/bin/env -i fetch --timeout=10 -o";
458# else
459 return "";
460# endif // __OpenBSD__
461#endif // defined(_WIN32)
462 }
463
465 double get_time_diff_to_current(std::string const & str_time) const
466 {
467 namespace co = std::chrono;
468 double curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
469
470 double d_time{};
471 std::from_chars(str_time.data(), str_time.data() + str_time.size(), d_time);
472
473 return curr - d_time;
474 }
475
479 std::array<int, 3> get_numbers_from_version_string(std::string const & str) const
480 {
481 std::array<int, 3> result{};
482
483 if (!std::regex_match(str, version_regex))
484 return result;
485
486 auto res = std::from_chars(str.data(), str.data() + str.size(), result[0]); // stops and sets res.ptr at '.'
487 res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[1]);
488 res = std::from_chars(res.ptr + 1, str.data() + str.size(), result[2]);
489
490 return result;
491 }
492
497 template <typename msg_type>
498 void write_cookie(msg_type && msg)
499 {
500 // The current time
501 namespace co = std::chrono;
502 auto curr = co::duration_cast<co::seconds>(co::system_clock::now().time_since_epoch()).count();
503
504 std::ofstream timestamp_file{timestamp_filename};
505
506 if (timestamp_file.is_open())
507 {
508 timestamp_file << curr << '\n' << msg;
509 timestamp_file.close();
510 }
511 }
512};
513
514} // 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 from_chars(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.
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