1 // ==========================================================================
2 //                 SeqAn - The Library for Sequence Analysis
3 // ==========================================================================
4 // Copyright (c) 2006-2018, Knut Reinert, FU Berlin
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions are met:
9 //
10 //     * Redistributions of source code must retain the above copyright
11 //       notice, this list of conditions and the following disclaimer.
12 //     * Redistributions in binary form must reproduce the above copyright
13 //       notice, this list of conditions and the following disclaimer in the
14 //       documentation and/or other materials provided with the distribution.
15 //     * Neither the name of Knut Reinert or the FU Berlin nor the names of
16 //       its contributors may be used to endorse or promote products derived
17 //       from this software without specific prior written permission.
18 //
19 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 // ARE DISCLAIMED. IN NO EVENT SHALL KNUT REINERT OR THE FU BERLIN BE LIABLE
23 // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28 // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
29 // DAMAGE.
30 //
31 // ==========================================================================
32 // Author: Svenja Mehringer <svenja.mehringer@fu-berlin.de>
33 // ==========================================================================
34 
35 #ifndef SEQAN_INCLUDE_ARG_PARSE_VERSION_CHECK_H_
36 #define SEQAN_INCLUDE_ARG_PARSE_VERSION_CHECK_H_
37 
38 #include <sys/stat.h>
39 
40 #include <iostream>
41 #include <fstream>
42 #include <regex>
43 #include <future>
44 #include <chrono>
45 
46 namespace seqan
47 {
48 
49 // ==========================================================================
50 // Forwards
51 // ==========================================================================
52 
53 // NOTE(rrahn): In-file forward for function call operator.
54 struct VersionCheck;
55 inline void _checkForNewerVersion(VersionCheck &, std::promise<bool>);
56 inline std::string _getPath();
57 constexpr const char * _getOS();
58 constexpr const char * _getBitSys();
59 
60 // ==========================================================================
61 // Tags, Classes, Enums
62 // ==========================================================================
63 
64 template <typename TVoidSpec = void>
65 struct VersionControlTags_
66 {
67     static constexpr char const * const SEQAN_NAME         = "seqan";
68     static constexpr char const * const UNREGISTERED_APP   = "UNREGISTERED_APP";
69 
70     static constexpr char const * const MESSAGE_SEQAN_UPDATE =
71         "[SEQAN INFO] :: A new SeqAn version is available online.\n"
72         "[SEQAN INFO] :: Please visit www.seqan.de for an update or inform the developer of this app.\n"
73         "[SEQAN INFO] :: If you don't wish to receive further notifications, set --version-check OFF.\n\n";
74     static constexpr char const * const MESSAGE_APP_UPDATE =
75         "[APP INFO] :: A new version of this application is now available.\n"
76         "[APP INFO] :: Visit www.seqan.de for updates of official SeqAn applications.\n"
77         "[APP INFO] :: If you don't wish to receive further notifications, set --version-check OFF.\n\n";
78     static constexpr char const * const MESSAGE_UNREGISTERED_APP =
79         "[SEQAN INFO] :: Thank you for using SeqAn!\n"
80         "[SEQAN INFO] :: Do you wish to register your app for update notifications?\n"
81         "[SEQAN INFO] :: Just send an email to support@seqan.de with your app name and version number.\n"
82         "[SEQAN INFO] :: If you don't wish to receive further notifications, set --version-check OFF.\n\n";
83     static constexpr char const * const MESSAGE_REGISTERED_APP_UPDATE =
84         "[APP INFO] :: We noticed the app version you use is newer than the one registered with us.\n"
85         "[APP INFO] :: Please send us an email with the new version so we can correct it (support@seqan.de)\n\n";
86 };
87 
88 template <typename TVoidSpec>
89 constexpr char const * const VersionControlTags_<TVoidSpec>::SEQAN_NAME;
90 template <typename TVoidSpec>
91 constexpr char const * const VersionControlTags_<TVoidSpec>::UNREGISTERED_APP;
92 template <typename TVoidSpec>
93 constexpr char const * const VersionControlTags_<TVoidSpec>::MESSAGE_SEQAN_UPDATE;
94 template <typename TVoidSpec>
95 constexpr char const * const VersionControlTags_<TVoidSpec>::MESSAGE_APP_UPDATE;
96 template <typename TVoidSpec>
97 constexpr char const * const VersionControlTags_<TVoidSpec>::MESSAGE_UNREGISTERED_APP;
98 template <typename TVoidSpec>
99 constexpr char const * const VersionControlTags_<TVoidSpec>::MESSAGE_REGISTERED_APP_UPDATE;
100 
101 struct VersionCheck
102 {
103     // ----------------------------------------------------------------------------
104     // Member Variables
105     // ----------------------------------------------------------------------------
106     std::string _url;
107     std::string _name;
108     std::string _version = "0.0.0";
109     std::string _program;
110     std::string _command;
111     std::string _path = _getPath();
112     std::string _timestamp_filename;
113     std::ostream & errorStream;
114 
115     // ----------------------------------------------------------------------------
116     // Constructors
117     // ----------------------------------------------------------------------------
118 
VersionCheckVersionCheck119     VersionCheck(std::string name,
120                  std::string const & version,
121                  std::ostream & errorStream) :
122         _name{std::move(name)},
123         errorStream(errorStream)
124     {
125         std::smatch versionMatch;
126 #if defined(NDEBUG) || defined(SEQAN_TEST_VERSION_CHECK_)
127         _timestamp_filename = _path + "/" + _name + "_usr.timestamp";
128 #else
129         _timestamp_filename = _path + "/" + _name + "_dev.timestamp";
130 #endif
131         if (!version.empty() &&
132             std::regex_search(version, versionMatch, std::regex("^([[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+).*")))
133         {
134             _version = versionMatch.str(1); // in case the git revision number is given take only version number
135         }
136         _url = static_cast<std::string>("http://seqan-update.informatik.uni-tuebingen.de/check/SeqAn_") + _getOS() + _getBitSys() + _name + "_" + _version;
137         _getProgram();
138         _updateCommand();
139     }
140 
141     // ----------------------------------------------------------------------------
142     // Member Functions
143     // ----------------------------------------------------------------------------
144 
145 #if defined(STDLIB_VS)
_getProgramVersionCheck146     void _getProgram()
147     {
148         _program = "powershell.exe -NoLogo -NonInteractive -Command \"& {Invoke-WebRequest -erroraction 'silentlycontinue' -OutFile";
149     }
150 #else  // Unix based platforms.
_getProgramVersionCheck151     void _getProgram()
152     {
153         // ask if system call for version or help is successfull
154         if (!system("wget --version > /dev/null 2>&1"))
155             _program = "wget -q -O";
156         else if (!system("curl --version > /dev/null 2>&1"))
157             _program =  "curl -o";
158 #ifndef __linux  // ftp call does not work on linux
159         else if (!system("which ftp > /dev/null 2>&1"))
160             _program =  "ftp -Vo";
161 #endif
162         else
163             _program.clear();
164     }
165 #endif  // defined(STDLIB_VS)
166 
_updateCommandVersionCheck167     void _updateCommand()
168     {
169         if (!_program.empty())
170         {
171             _command = _program + " " + _path + "/" + _name + ".version " + _url;
172 #if defined(STDLIB_VS)
173             _command = _command + "; exit  [int] -not $?}\" > nul 2>&1";
174 #else
175             _command = _command + " > /dev/null 2>&1";
176 #endif
177         }
178     }
179 
operatorVersionCheck180     inline void operator()(std::promise<bool> versionCheckProm)
181     {
182         _checkForNewerVersion(*this, std::move(versionCheckProm));
183     }
184 };
185 
186 // ==========================================================================
187 // Metafunctions
188 // ==========================================================================
189 
190 // ==========================================================================
191 // Functions
192 // ==========================================================================
193 
194 // ----------------------------------------------------------------------------
195 // Function setURL()
196 // ----------------------------------------------------------------------------
197 
setURL(VersionCheck & me,std::string url)198 inline void setURL(VersionCheck & me, std::string url)
199 {
200     std::swap(me._url, url);
201     me._updateCommand();
202 }
203 
204 // ----------------------------------------------------------------------------
205 // Function _getOS()
206 // ----------------------------------------------------------------------------
207 
_getOS()208 constexpr const char * _getOS()
209 {
210     //get system information
211 #ifdef __linux
212     return "Linux";
213 #elif __APPLE__
214     return "MacOS";
215 #elif defined(STDLIB_VS)
216     return "Windows";
217 #elif __FreeBSD__
218     return "FreeBSD";
219 #elif __OpenBSD__
220     return "OpenBSD";
221 #else
222     return "unknown";
223 #endif
224 }
225 
226 // ----------------------------------------------------------------------------
227 // Function _getBitSys()
228 // ----------------------------------------------------------------------------
229 
_getBitSys()230 constexpr const char * _getBitSys()
231 {
232 #if SEQAN_IS_32_BIT
233     return "_32_";
234 #else
235      return "_64_";
236 #endif
237 }
238 
239 // ----------------------------------------------------------------------------
240 // Function _checkWritability()
241 // ----------------------------------------------------------------------------
242 
243 #if defined(STDLIB_VS)
_checkWritability(std::string const & path)244 inline bool _checkWritability(std::string const & path)
245 {
246     DWORD ftyp = GetFileAttributesA(path.c_str());
247     if (ftyp == INVALID_FILE_ATTRIBUTES || !(ftyp & FILE_ATTRIBUTE_DIRECTORY))
248     {
249         if (!CreateDirectory(path.c_str(), NULL))
250             return false;
251     }
252 
253     HANDLE dummyFile; // check writablity by trying to create a file in GENERIC_WRITE mode
254     std::string fileName(path + "/dummy.txt");
255     dummyFile = CreateFile(fileName.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
256 
257     if (dummyFile == INVALID_HANDLE_VALUE)
258         return false;
259 
260     CloseHandle(dummyFile);
261     bool successful_deletion = DeleteFile(fileName.c_str());
262     SEQAN_ASSERT(successful_deletion);
263     if (!successful_deletion)
264         return false;
265 
266     return true;
267 }
268 #else
_checkWritability(std::string const & path)269 inline bool _checkWritability(std::string const & path)
270 {
271     struct stat d_stat;
272     if (stat(path.c_str(), &d_stat) < 0)
273     {
274         // try to make dir
275         std::string makeDir("mkdir -p " + path);
276         if (system(makeDir.c_str()))
277             return false; // could not create home dir
278 
279         if (stat(path.c_str(), &d_stat) < 0) // repeat stat
280             return false;
281     }
282 
283     if (!(d_stat.st_mode & S_IWUSR))
284         return false; // dir not writable
285 
286     return true;
287 }
288 #endif
289 
290 // ----------------------------------------------------------------------------
291 // Function _getPath()
292 // ----------------------------------------------------------------------------
293 
_getPath()294 inline std::string _getPath()
295 {
296     std::string path;
297 #if defined(STDLIB_VS)
298     path = std::string(getenv("UserProfile")) + "/.config/seqan";
299 #else
300     path = std::string(getenv("HOME")) + "/.config/seqan";
301 #endif
302 
303     // check if user has permission to write to home path
304     if (!_checkWritability(path))
305     {
306 #if defined(STDLIB_VS)
307         TCHAR tmp_path [MAX_PATH];
308         if (GetTempPath(MAX_PATH, tmp_path) != 0)
309         {
310             path = tmp_path;
311         }
312         else
313         { //GetTempPath() returns 0 on failure
314             path.clear();
315         }
316 # else // unix
317         path = "/tmp";
318 #endif
319     }
320     return path;
321 }
322 
323 // ----------------------------------------------------------------------------
324 // Function _getFileTimeDiff()
325 // ----------------------------------------------------------------------------
326 
_getFileTimeDiff(VersionCheck const & me)327 inline double _getFileTimeDiff(VersionCheck const & me)
328 {
329     double curr = static_cast<double>(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count());
330     std::ifstream timestamp_file;
331     timestamp_file.open(me._timestamp_filename.c_str());
332 
333     if (timestamp_file.is_open())
334     {
335         std::string str_time;
336         std::getline(timestamp_file, str_time);
337         timestamp_file.close();
338         double d_time;
339         lexicalCast(d_time, str_time);
340         return curr - d_time;
341     }
342     return curr;
343 }
344 
345 // ----------------------------------------------------------------------------
346 // Function _getNumbersFromString()
347 // ----------------------------------------------------------------------------
348 
_getNumbersFromString(std::string const & str)349 inline String<int> _getNumbersFromString(std::string const & str)
350 {
351     String<int> numbers;
352     StringSet<std::string> set;
353     strSplit(set, str, EqualsChar<'.'>(), false);
354     for (auto & num : set)
355     {
356         appendValue(numbers, lexicalCast<int>(num));
357     }
358     return numbers;
359 }
360 
361 // ----------------------------------------------------------------------------
362 // Function _readVersionString()
363 // ----------------------------------------------------------------------------
364 
_readVersionStrings(std::vector<std::string> & versions,std::string const & version_file)365 inline void _readVersionStrings(std::vector<std::string> & versions, std::string const & version_file)
366 {
367     std::ifstream myfile;
368     myfile.open(version_file.c_str());
369     std::string app_version;
370     std::string seqan_version;
371     if (myfile.is_open())
372     {
373         std::getline(myfile, app_version); // get first line which should only contain the version number of the app
374 
375 #if !defined(NDEBUG) || defined(SEQAN_TEST_VERSION_CHECK_)
376         if (app_version == VersionControlTags_<>::UNREGISTERED_APP)
377             versions[0] = app_version;
378 #endif // !defined(NDEBUG) || defined(SEQAN_TEST_VERSION_CHECK_)
379 
380         if (std::regex_match(app_version, std::regex("^[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+$")))
381             versions[0] = app_version;
382 
383         std::getline(myfile, seqan_version); // get second line which should only contain the version number of seqan
384 
385         if (std::regex_match(seqan_version, std::regex("^[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+$")))
386             versions[1] = seqan_version;
387 
388         myfile.close();
389     }
390 }
391 
392 // ----------------------------------------------------------------------------
393 // Function _callServer()
394 // ----------------------------------------------------------------------------
395 
_callServer(VersionCheck const me,std::promise<bool> prom)396 inline void _callServer(VersionCheck const me, std::promise<bool> prom)
397 {
398     // update timestamp
399     std::ofstream timestamp_file(me._timestamp_filename.c_str());
400     if (timestamp_file.is_open())
401     {
402         timestamp_file << std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
403         timestamp_file.close();
404     }
405 
406     // system call
407     // http response is stored in a file '.config/seqan/{app_name}_version'
408     if (system(me._command.c_str()))
409         prom.set_value(false);
410     else
411         prom.set_value(true);
412 }
413 
414 // ----------------------------------------------------------------------------
415 // Function checkForNewerVersion()
416 // ----------------------------------------------------------------------------
417 
_checkForNewerVersion(VersionCheck & me,std::promise<bool> prom)418 inline void _checkForNewerVersion(VersionCheck & me, std::promise<bool> prom)
419 {
420     if (me._path.empty()) // neither home dir nor temp dir are writable
421     {
422         prom.set_value(false);
423         return;
424     }
425 
426     std::string version_filename(me._path + "/" + me._name + ".version");
427     double min_time_diff(86400);                                 // one day = 86400 seonds
428     double file_time_diff(_getFileTimeDiff(me)); // time difference in seconds
429 
430     if (file_time_diff < min_time_diff)
431     {
432         prom.set_value(false); // only check for newer version once a day
433         return;
434     }
435 
436     std::vector<std::string> str_server_versions{"", ""};
437     _readVersionStrings(str_server_versions, version_filename);
438 
439 #if !defined(NDEBUG) || defined(SEQAN_TEST_VERSION_CHECK_) // only check seqan version in debug or testing mode
440     if (!str_server_versions[1].empty()) // seqan version
441     {
442         std::string seqan_version = std::to_string(SEQAN_VERSION_MAJOR) + "." +
443                                     std::to_string(SEQAN_VERSION_MINOR) + "." +
444                                     std::to_string(SEQAN_VERSION_PATCH);
445         Lexical<> version_comp(_getNumbersFromString(seqan_version), _getNumbersFromString(str_server_versions[1]));
446 
447         if (isLess(version_comp))
448             me.errorStream << VersionControlTags_<>::MESSAGE_SEQAN_UPDATE;
449     }
450     if (str_server_versions[0] == VersionControlTags_<>::UNREGISTERED_APP)
451         me.errorStream << VersionControlTags_<>::MESSAGE_UNREGISTERED_APP;
452 #endif
453 
454     if (!str_server_versions[0].empty() & !(str_server_versions[0] == VersionControlTags_<>::UNREGISTERED_APP)) // app version
455     {
456         Lexical<> version_comp(_getNumbersFromString(me._version), _getNumbersFromString(str_server_versions[0]));
457 
458 #if defined(NDEBUG) || defined(SEQAN_TEST_VERSION_CHECK_) // only check app version in release or testing mode
459         if (isLess(version_comp))
460             me.errorStream << VersionControlTags_<>::MESSAGE_APP_UPDATE;
461 #endif // defined(NDEBUG) || defined(SEQAN_TEST_VERSION_CHECK_)
462 
463 #if !defined(NDEBUG) || defined(SEQAN_TEST_VERSION_CHECK_) // only notify developer that app version should be updated on server
464         if (isGreater(version_comp))
465             me.errorStream << VersionControlTags_<>::MESSAGE_REGISTERED_APP_UPDATE;
466 #endif // !defined(NDEBUG) || defined(SEQAN_TEST_VERSION_CHECK_)
467     }
468 
469     if (me._program.empty())
470     {
471         prom.set_value(false);
472         return;
473     }
474 
475     // launch a seperate thread to not defer runtime.
476     std::thread(_callServer, me, std::move(prom)).detach();
477 }
478 
479 } // namespace seqan
480 #endif //SEQAN_INCLUDE_ARG_PARSE_VERSION_CHECK_H_
481