Implemented non-blocking download for the repository index list
[supertux.git] / src / addon / addon_manager.cpp
1 //  SuperTux - Add-on Manager
2 //  Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
3 //                2014 Ingo Ruhnke <grumbel@gmail.com>
4 //
5 //  This program is free software: you can redistribute it and/or modify
6 //  it under the terms of the GNU General Public License as published by
7 //  the Free Software Foundation, either version 3 of the License, or
8 //  (at your option) any later version.
9 //
10 //  This program is distributed in the hope that it will be useful,
11 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 //  GNU General Public License for more details.
14 //
15 //  You should have received a copy of the GNU General Public License
16 //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 #include "addon/addon_manager.hpp"
19
20 #include <config.h>
21 #include <version.h>
22
23 #include <algorithm>
24 #include <iostream>
25 #include <memory>
26 #include <physfs.h>
27 #include <sstream>
28 #include <stdexcept>
29 #include <stdio.h>
30 #include <sys/stat.h>
31
32 #include "addon/addon.hpp"
33 #include "addon/md5.hpp"
34 #include "lisp/list_iterator.hpp"
35 #include "lisp/parser.hpp"
36 #include "util/file_system.hpp"
37 #include "util/log.hpp"
38 #include "util/reader.hpp"
39 #include "util/writer.hpp"
40
41 namespace {
42
43 MD5 md5_from_file(const std::string& filename)
44 {
45   // TODO: this does not work as expected for some files -- IFileStream seems to not always behave like an ifstream.
46   //IFileStream ifs(installed_physfs_filename);
47   //std::string md5 = MD5(ifs).hex_digest();
48
49   MD5 md5;
50
51   unsigned char buffer[1024];
52   PHYSFS_file* file = PHYSFS_openRead(filename.c_str());
53   if (!file)
54   {
55     std::ostringstream out;
56     out << "PHYSFS_openRead() failed: " << PHYSFS_getLastError();
57     throw std::runtime_error(out.str());
58   }
59   else
60   {
61     while (true)
62     {
63       PHYSFS_sint64 len = PHYSFS_read(file, buffer, 1, sizeof(buffer));
64       if (len <= 0) break;
65       md5.update(buffer, len);
66     }
67     PHYSFS_close(file);
68
69     return md5;
70   }
71 }
72
73 bool has_suffix(const std::string& str, const std::string& suffix)
74 {
75   if (str.length() >= suffix.length())
76     return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
77   else
78     return false;
79 }
80
81 } // namespace
82
83 AddonManager::AddonManager(const std::string& addon_directory,
84                            std::vector<Config::Addon>& addon_config) :
85   m_downloader(),
86   m_addon_directory(addon_directory),
87   m_repository_url("http://addons.supertux.googlecode.com/git/index-0_4_0.nfo"),
88   m_addon_config(addon_config),
89   m_installed_addons(),
90   m_repository_addons(),
91   m_has_been_updated(false),
92   m_install_request(),
93   m_install_status(),
94   m_transfer_status()
95 {
96   PHYSFS_mkdir(m_addon_directory.c_str());
97
98   add_installed_addons();
99
100   // FIXME: We should also restore the order here
101   for(auto& addon : m_addon_config)
102   {
103     if (addon.enabled)
104     {
105       try
106       {
107         enable_addon(addon.id);
108       }
109       catch(const std::exception& err)
110       {
111         log_warning << "failed to enable addon from config: " << err.what() << std::endl;
112       }
113     }
114   }
115 }
116
117 AddonManager::~AddonManager()
118 {
119   // sync enabled/disabled addons into the config for saving
120   m_addon_config.clear();
121   for(auto& addon : m_installed_addons)
122   {
123     m_addon_config.push_back({addon->get_id(), addon->is_enabled()});
124   }
125 }
126
127 Addon&
128 AddonManager::get_repository_addon(const AddonId& id)
129 {
130   auto it = std::find_if(m_repository_addons.begin(), m_repository_addons.end(),
131                          [&id](const std::unique_ptr<Addon>& addon)
132                          {
133                            return addon->get_id() == id;
134                          });
135
136   if (it != m_repository_addons.end())
137   {
138     return **it;
139   }
140   else
141   {
142     throw std::runtime_error("Couldn't find repository Addon with id: " + id);
143   }
144 }
145
146 Addon&
147 AddonManager::get_installed_addon(const AddonId& id)
148 {
149   auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
150                          [&id](const std::unique_ptr<Addon>& addon)
151                          {
152                            return addon->get_id() == id;
153                          });
154
155   if (it != m_installed_addons.end())
156   {
157     return **it;
158   }
159   else
160   {
161     throw std::runtime_error("Couldn't find installed Addon with id: " + id);
162   }
163 }
164
165 std::vector<AddonId>
166 AddonManager::get_repository_addons() const
167 {
168   std::vector<AddonId> results;
169   results.reserve(m_repository_addons.size());
170   std::transform(m_repository_addons.begin(), m_repository_addons.end(),
171                  std::back_inserter(results),
172                  [](const std::unique_ptr<Addon>& addon)
173                  {
174                    return addon->get_id();
175                  });
176   return results;
177 }
178
179
180 std::vector<AddonId>
181 AddonManager::get_installed_addons() const
182 {
183   std::vector<AddonId> results;
184   results.reserve(m_installed_addons.size());
185   std::transform(m_installed_addons.begin(), m_installed_addons.end(),
186                  std::back_inserter(results),
187                  [](const std::unique_ptr<Addon>& addon)
188                  {
189                    return addon->get_id();
190                  });
191   return results;
192 }
193
194 bool
195 AddonManager::has_online_support() const
196 {
197   return true;
198 }
199
200 bool
201 AddonManager::has_been_updated() const
202 {
203   return m_has_been_updated;
204 }
205
206 AddonManager::InstallStatusPtr
207 AddonManager::request_check_online()
208 {
209   if (m_install_status)
210   {
211     throw std::runtime_error("only one addon install request allowed at a time");
212   }
213   else
214   {
215     m_transfer_status = m_downloader.request_download(m_repository_url, "/addons/repository.nfo");
216
217     m_transfer_status->then([this]{
218         m_repository_addons = parse_addon_infos("/addons/repository.nfo");
219         m_has_been_updated = true;
220
221         if (m_install_status->callback)
222         {
223           m_install_status->callback();
224         }
225
226         m_install_status = {};
227         m_transfer_status = {};
228       });
229
230     m_install_status = std::make_shared<InstallStatus>();
231     return m_install_status;
232   }
233 }
234
235 void
236 AddonManager::check_online()
237 {
238   m_downloader.download(m_repository_url, "/addons/repository.nfo");
239   m_repository_addons = parse_addon_infos("/addons/repository.nfo");
240   m_has_been_updated = true;
241 }
242
243 AddonManager::InstallStatusPtr
244 AddonManager::request_install_addon(const AddonId& addon_id)
245 {
246   if (m_install_status)
247   {
248     throw std::runtime_error("only one addon install request allowed at a time");
249   }
250   else
251   {
252     { // remove addon if it already exists
253       auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
254                              [&addon_id](const std::unique_ptr<Addon>& addon)
255                              {
256                                return addon->get_id() == addon_id;
257                              });
258       if (it != m_installed_addons.end())
259       {
260         log_debug << "reinstalling addon " << addon_id << std::endl;
261         if ((*it)->is_enabled())
262         {
263           disable_addon((*it)->get_id());
264         }
265         m_installed_addons.erase(it);
266       }
267       else
268       {
269         log_debug << "installing addon " << addon_id << std::endl;
270       }
271     }
272
273     {
274       Addon& repository_addon = get_repository_addon(addon_id);
275
276       m_install_request = std::make_shared<InstallRequest>();
277       m_install_request->install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
278       m_install_request->addon_id = addon_id;
279
280       m_transfer_status = m_downloader.request_download(repository_addon.get_url(),
281                                                         m_install_request->install_filename);
282     }
283
284     m_transfer_status->then(
285       [this]
286       {
287         // complete the addon install
288         Addon& repository_addon = get_repository_addon(m_install_request->addon_id);
289
290         MD5 md5 = md5_from_file(m_install_request->install_filename);
291         if (repository_addon.get_md5() != md5.hex_digest())
292         {
293           if (PHYSFS_delete(m_install_request->install_filename.c_str()) == 0)
294           {
295             log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
296           }
297
298           throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
299         }
300         else
301         {
302           const char* realdir = PHYSFS_getRealDir(m_install_request->install_filename.c_str());
303           if (!realdir)
304           {
305             throw std::runtime_error("PHYSFS_getRealDir failed: " + m_install_request->install_filename);
306           }
307           else
308           {
309             add_installed_archive(m_install_request->install_filename, md5.hex_digest());
310           }
311         }
312
313         // signal that the request is done and cleanup
314         if (m_install_status->callback)
315         {
316           m_install_status->callback();
317         }
318
319         m_install_request = {};
320         m_install_status = {};
321         m_transfer_status = {};
322       });
323
324     m_install_status = std::make_shared<InstallStatus>();
325
326     return m_install_status;
327   }
328 }
329
330 void
331 AddonManager::install_addon(const AddonId& addon_id)
332 {
333   { // remove addon if it already exists
334     auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
335                            [&addon_id](const std::unique_ptr<Addon>& addon)
336                            {
337                              return addon->get_id() == addon_id;
338                            });
339     if (it != m_installed_addons.end())
340     {
341       log_debug << "reinstalling addon " << addon_id << std::endl;
342       if ((*it)->is_enabled())
343       {
344         disable_addon((*it)->get_id());
345       }
346       m_installed_addons.erase(it);
347     }
348     else
349     {
350       log_debug << "installing addon " << addon_id << std::endl;
351     }
352   }
353
354   Addon& repository_addon = get_repository_addon(addon_id);
355
356   std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
357
358   m_downloader.download(repository_addon.get_url(), install_filename);
359
360   MD5 md5 = md5_from_file(install_filename);
361   if (repository_addon.get_md5() != md5.hex_digest())
362   {
363     if (PHYSFS_delete(install_filename.c_str()) == 0)
364     {
365       log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
366     }
367
368     throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
369   }
370   else
371   {
372     const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
373     if (!realdir)
374     {
375       throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
376     }
377     else
378     {
379       add_installed_archive(install_filename, md5.hex_digest());
380     }
381   }
382 }
383
384 void
385 AddonManager::uninstall_addon(const AddonId& addon_id)
386 {
387   log_debug << "uninstalling addon " << addon_id << std::endl;
388   Addon& addon = get_installed_addon(addon_id);
389   if (addon.is_enabled())
390   {
391     disable_addon(addon_id);
392   }
393   log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
394   PHYSFS_delete(addon.get_install_filename().c_str());
395   m_installed_addons.erase(std::remove_if(m_installed_addons.begin(), m_installed_addons.end(),
396                                           [&addon](const std::unique_ptr<Addon>& rhs)
397                                           {
398                                             return addon.get_id() == rhs->get_id();
399                                           }),
400                            m_installed_addons.end());
401 }
402
403 void
404 AddonManager::enable_addon(const AddonId& addon_id)
405 {
406   log_debug << "enabling addon " << addon_id << std::endl;
407   Addon& addon = get_installed_addon(addon_id);
408   if (addon.is_enabled())
409   {
410     log_warning << "Tried enabling already enabled Add-on" << std::endl;
411   }
412   else
413   {
414     log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
415     //int PHYSFS_mount(addon.installed_install_filename.c_str(), "addons/", 0)
416     if (PHYSFS_addToSearchPath(addon.get_install_filename().c_str(), 0) == 0)
417     {
418       log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
419                   << PHYSFS_getLastError() << std::endl;
420     }
421     else
422     {
423       addon.set_enabled(true);
424     }
425   }
426 }
427
428 void
429 AddonManager::disable_addon(const AddonId& addon_id)
430 {
431   log_debug << "disabling addon " << addon_id << std::endl;
432   Addon& addon = get_installed_addon(addon_id);
433   if (!addon.is_enabled())
434   {
435     log_warning << "Tried disabling already disabled Add-On" << std::endl;
436   }
437   else
438   {
439     log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
440     if (PHYSFS_removeFromSearchPath(addon.get_install_filename().c_str()) == 0)
441     {
442       log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
443                   << PHYSFS_getLastError() << std::endl;
444     }
445     else
446     {
447       addon.set_enabled(false);
448     }
449   }
450 }
451
452 std::vector<std::string>
453 AddonManager::scan_for_archives() const
454 {
455   std::vector<std::string> archives;
456
457   // Search for archives and add them to the search path
458   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
459     rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()),
460        PHYSFS_freeList);
461   for(char** i = rc.get(); *i != 0; ++i)
462   {
463     if (has_suffix(*i, ".zip"))
464     {
465       std::string archive = FileSystem::join(m_addon_directory, *i);
466       if (PHYSFS_exists(archive.c_str()))
467       {
468         archives.push_back(archive);
469       }
470     }
471   }
472
473   return archives;
474 }
475
476 std::string
477 AddonManager::scan_for_info(const std::string& archive_os_path) const
478 {
479   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
480     rc2(PHYSFS_enumerateFiles("/"),
481         PHYSFS_freeList);
482   for(char** j = rc2.get(); *j != 0; ++j)
483   {
484     if (has_suffix(*j, ".nfo"))
485     {
486       std::string nfo_filename = FileSystem::join("/", *j);
487
488       // make sure it's in the current archive_os_path
489       const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
490       if (!realdir)
491       {
492         log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastError() << std::endl;
493       }
494       else
495       {
496         if (realdir == archive_os_path)
497         {
498           return nfo_filename;
499         }
500       }
501     }
502   }
503
504   return std::string();
505 }
506
507 void
508 AddonManager::add_installed_archive(const std::string& archive, const std::string& md5)
509 {
510   const char* realdir = PHYSFS_getRealDir(archive.c_str());
511   if (!realdir)
512   {
513     log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
514                 << PHYSFS_getLastError() << std::endl;
515   }
516   else
517   {
518     std::string os_path = FileSystem::join(realdir, archive);
519
520     PHYSFS_addToSearchPath(os_path.c_str(), 0);
521
522     std::string nfo_filename = scan_for_info(os_path);
523
524     if (nfo_filename.empty())
525     {
526       log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
527     }
528     else
529     {
530       try
531       {
532         std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
533         addon->set_install_filename(os_path, md5);
534         m_installed_addons.push_back(std::move(addon));
535       }
536       catch (const std::runtime_error& e)
537       {
538         log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
539       }
540     }
541
542     PHYSFS_removeFromSearchPath(os_path.c_str());
543   }
544 }
545
546 void
547 AddonManager::add_installed_addons()
548 {
549   auto archives = scan_for_archives();
550
551   for(auto archive : archives)
552   {
553     MD5 md5 = md5_from_file(archive);
554     add_installed_archive(archive, md5.hex_digest());
555   }
556 }
557
558 AddonManager::AddonList
559 AddonManager::parse_addon_infos(const std::string& filename) const
560 {
561   AddonList m_addons;
562
563   try
564   {
565     lisp::Parser parser;
566     const lisp::Lisp* root = parser.parse(filename);
567     const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
568     if(!addons_lisp)
569     {
570       throw std::runtime_error("Downloaded file is not an Add-on list");
571     }
572     else
573     {
574       lisp::ListIterator iter(addons_lisp);
575       while(iter.next())
576       {
577         const std::string& token = iter.item();
578         if(token != "supertux-addoninfo")
579         {
580           log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
581         }
582         else
583         {
584           std::unique_ptr<Addon> addon = Addon::parse(*iter.lisp());
585           m_addons.push_back(std::move(addon));
586         }
587       }
588
589       return m_addons;
590     }
591   }
592   catch(const std::exception& e)
593   {
594     std::stringstream msg;
595     msg << "Problem when reading Add-on list: " << e.what();
596     throw std::runtime_error(msg.str());
597   }
598
599   return m_addons;
600 }
601
602 void
603 AddonManager::update()
604 {
605   m_downloader.update();
606
607   if (m_install_status)
608   {
609     m_install_status->now = m_transfer_status->dlnow;
610     m_install_status->total = m_transfer_status->dltotal;
611   }
612 }
613
614 void
615 AddonManager::abort_install()
616 {
617   log_info << "addon install aborted" << std::endl;
618
619   m_downloader.abort(m_transfer_status->id);
620
621   m_install_request = {};
622   m_install_status = {};
623   m_transfer_status = {};
624 }
625
626 /* EOF */