Use a chain of callbacks instead of a single one in TransferStatusPtr
[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_transfer_status()
94 {
95   PHYSFS_mkdir(m_addon_directory.c_str());
96
97   add_installed_addons();
98
99   // FIXME: We should also restore the order here
100   for(auto& addon : m_addon_config)
101   {
102     if (addon.enabled)
103     {
104       try
105       {
106         enable_addon(addon.id);
107       }
108       catch(const std::exception& err)
109       {
110         log_warning << "failed to enable addon from config: " << err.what() << std::endl;
111       }
112     }
113   }
114 }
115
116 AddonManager::~AddonManager()
117 {
118   // sync enabled/disabled addons into the config for saving
119   m_addon_config.clear();
120   for(auto& addon : m_installed_addons)
121   {
122     m_addon_config.push_back({addon->get_id(), addon->is_enabled()});
123   }
124 }
125
126 Addon&
127 AddonManager::get_repository_addon(const AddonId& id)
128 {
129   auto it = std::find_if(m_repository_addons.begin(), m_repository_addons.end(),
130                          [&id](const std::unique_ptr<Addon>& addon)
131                          {
132                            return addon->get_id() == id;
133                          });
134
135   if (it != m_repository_addons.end())
136   {
137     return **it;
138   }
139   else
140   {
141     throw std::runtime_error("Couldn't find repository Addon with id: " + id);
142   }
143 }
144
145 Addon&
146 AddonManager::get_installed_addon(const AddonId& id)
147 {
148   auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
149                          [&id](const std::unique_ptr<Addon>& addon)
150                          {
151                            return addon->get_id() == id;
152                          });
153
154   if (it != m_installed_addons.end())
155   {
156     return **it;
157   }
158   else
159   {
160     throw std::runtime_error("Couldn't find installed Addon with id: " + id);
161   }
162 }
163
164 std::vector<AddonId>
165 AddonManager::get_repository_addons() const
166 {
167   std::vector<AddonId> results;
168   results.reserve(m_repository_addons.size());
169   std::transform(m_repository_addons.begin(), m_repository_addons.end(),
170                  std::back_inserter(results),
171                  [](const std::unique_ptr<Addon>& addon)
172                  {
173                    return addon->get_id();
174                  });
175   return results;
176 }
177
178
179 std::vector<AddonId>
180 AddonManager::get_installed_addons() const
181 {
182   std::vector<AddonId> results;
183   results.reserve(m_installed_addons.size());
184   std::transform(m_installed_addons.begin(), m_installed_addons.end(),
185                  std::back_inserter(results),
186                  [](const std::unique_ptr<Addon>& addon)
187                  {
188                    return addon->get_id();
189                  });
190   return results;
191 }
192
193 bool
194 AddonManager::has_online_support() const
195 {
196   return true;
197 }
198
199 bool
200 AddonManager::has_been_updated() const
201 {
202   return m_has_been_updated;
203 }
204
205 TransferStatusPtr
206 AddonManager::request_check_online()
207 {
208   if (m_transfer_status)
209   {
210     throw std::runtime_error("only async request can be made to AddonManager at a time");
211   }
212   else
213   {
214     m_transfer_status = m_downloader.request_download(m_repository_url, "/addons/repository.nfo");
215
216     m_transfer_status->then([this]{
217         m_repository_addons = parse_addon_infos("/addons/repository.nfo");
218         m_has_been_updated = true;
219
220         m_transfer_status = {};
221       });
222
223     return m_transfer_status;
224   }
225 }
226
227 void
228 AddonManager::check_online()
229 {
230   m_downloader.download(m_repository_url, "/addons/repository.nfo");
231   m_repository_addons = parse_addon_infos("/addons/repository.nfo");
232   m_has_been_updated = true;
233 }
234
235 TransferStatusPtr
236 AddonManager::request_install_addon(const AddonId& addon_id)
237 {
238   if (m_transfer_status)
239   {
240     throw std::runtime_error("only one addon install request allowed at a time");
241   }
242   else
243   {
244     { // remove addon if it already exists
245       auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
246                              [&addon_id](const std::unique_ptr<Addon>& addon)
247                              {
248                                return addon->get_id() == addon_id;
249                              });
250       if (it != m_installed_addons.end())
251       {
252         log_debug << "reinstalling addon " << addon_id << std::endl;
253         if ((*it)->is_enabled())
254         {
255           disable_addon((*it)->get_id());
256         }
257         m_installed_addons.erase(it);
258       }
259       else
260       {
261         log_debug << "installing addon " << addon_id << std::endl;
262       }
263     }
264
265     {
266       Addon& repository_addon = get_repository_addon(addon_id);
267
268       m_install_request = std::make_shared<InstallRequest>();
269       m_install_request->install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
270       m_install_request->addon_id = addon_id;
271
272       m_transfer_status = m_downloader.request_download(repository_addon.get_url(),
273                                                         m_install_request->install_filename);
274     }
275
276     m_transfer_status->then(
277       [this]
278       {
279         // complete the addon install
280         Addon& repository_addon = get_repository_addon(m_install_request->addon_id);
281
282         MD5 md5 = md5_from_file(m_install_request->install_filename);
283         if (repository_addon.get_md5() != md5.hex_digest())
284         {
285           if (PHYSFS_delete(m_install_request->install_filename.c_str()) == 0)
286           {
287             log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
288           }
289
290           throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
291         }
292         else
293         {
294           const char* realdir = PHYSFS_getRealDir(m_install_request->install_filename.c_str());
295           if (!realdir)
296           {
297             throw std::runtime_error("PHYSFS_getRealDir failed: " + m_install_request->install_filename);
298           }
299           else
300           {
301             add_installed_archive(m_install_request->install_filename, md5.hex_digest());
302           }
303         }
304
305         m_install_request = {};
306         m_transfer_status = {};
307       });
308
309     return m_transfer_status;
310   }
311 }
312
313 void
314 AddonManager::install_addon(const AddonId& addon_id)
315 {
316   { // remove addon if it already exists
317     auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
318                            [&addon_id](const std::unique_ptr<Addon>& addon)
319                            {
320                              return addon->get_id() == addon_id;
321                            });
322     if (it != m_installed_addons.end())
323     {
324       log_debug << "reinstalling addon " << addon_id << std::endl;
325       if ((*it)->is_enabled())
326       {
327         disable_addon((*it)->get_id());
328       }
329       m_installed_addons.erase(it);
330     }
331     else
332     {
333       log_debug << "installing addon " << addon_id << std::endl;
334     }
335   }
336
337   Addon& repository_addon = get_repository_addon(addon_id);
338
339   std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
340
341   m_downloader.download(repository_addon.get_url(), install_filename);
342
343   MD5 md5 = md5_from_file(install_filename);
344   if (repository_addon.get_md5() != md5.hex_digest())
345   {
346     if (PHYSFS_delete(install_filename.c_str()) == 0)
347     {
348       log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
349     }
350
351     throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
352   }
353   else
354   {
355     const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
356     if (!realdir)
357     {
358       throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
359     }
360     else
361     {
362       add_installed_archive(install_filename, md5.hex_digest());
363     }
364   }
365 }
366
367 void
368 AddonManager::uninstall_addon(const AddonId& addon_id)
369 {
370   log_debug << "uninstalling addon " << addon_id << std::endl;
371   Addon& addon = get_installed_addon(addon_id);
372   if (addon.is_enabled())
373   {
374     disable_addon(addon_id);
375   }
376   log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
377   PHYSFS_delete(addon.get_install_filename().c_str());
378   m_installed_addons.erase(std::remove_if(m_installed_addons.begin(), m_installed_addons.end(),
379                                           [&addon](const std::unique_ptr<Addon>& rhs)
380                                           {
381                                             return addon.get_id() == rhs->get_id();
382                                           }),
383                            m_installed_addons.end());
384 }
385
386 void
387 AddonManager::enable_addon(const AddonId& addon_id)
388 {
389   log_debug << "enabling addon " << addon_id << std::endl;
390   Addon& addon = get_installed_addon(addon_id);
391   if (addon.is_enabled())
392   {
393     log_warning << "Tried enabling already enabled Add-on" << std::endl;
394   }
395   else
396   {
397     log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
398     //int PHYSFS_mount(addon.installed_install_filename.c_str(), "addons/", 0)
399     if (PHYSFS_addToSearchPath(addon.get_install_filename().c_str(), 0) == 0)
400     {
401       log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
402                   << PHYSFS_getLastError() << std::endl;
403     }
404     else
405     {
406       addon.set_enabled(true);
407     }
408   }
409 }
410
411 void
412 AddonManager::disable_addon(const AddonId& addon_id)
413 {
414   log_debug << "disabling addon " << addon_id << std::endl;
415   Addon& addon = get_installed_addon(addon_id);
416   if (!addon.is_enabled())
417   {
418     log_warning << "Tried disabling already disabled Add-On" << std::endl;
419   }
420   else
421   {
422     log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
423     if (PHYSFS_removeFromSearchPath(addon.get_install_filename().c_str()) == 0)
424     {
425       log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
426                   << PHYSFS_getLastError() << std::endl;
427     }
428     else
429     {
430       addon.set_enabled(false);
431     }
432   }
433 }
434
435 std::vector<std::string>
436 AddonManager::scan_for_archives() const
437 {
438   std::vector<std::string> archives;
439
440   // Search for archives and add them to the search path
441   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
442     rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()),
443        PHYSFS_freeList);
444   for(char** i = rc.get(); *i != 0; ++i)
445   {
446     if (has_suffix(*i, ".zip"))
447     {
448       std::string archive = FileSystem::join(m_addon_directory, *i);
449       if (PHYSFS_exists(archive.c_str()))
450       {
451         archives.push_back(archive);
452       }
453     }
454   }
455
456   return archives;
457 }
458
459 std::string
460 AddonManager::scan_for_info(const std::string& archive_os_path) const
461 {
462   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
463     rc2(PHYSFS_enumerateFiles("/"),
464         PHYSFS_freeList);
465   for(char** j = rc2.get(); *j != 0; ++j)
466   {
467     if (has_suffix(*j, ".nfo"))
468     {
469       std::string nfo_filename = FileSystem::join("/", *j);
470
471       // make sure it's in the current archive_os_path
472       const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
473       if (!realdir)
474       {
475         log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastError() << std::endl;
476       }
477       else
478       {
479         if (realdir == archive_os_path)
480         {
481           return nfo_filename;
482         }
483       }
484     }
485   }
486
487   return std::string();
488 }
489
490 void
491 AddonManager::add_installed_archive(const std::string& archive, const std::string& md5)
492 {
493   const char* realdir = PHYSFS_getRealDir(archive.c_str());
494   if (!realdir)
495   {
496     log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
497                 << PHYSFS_getLastError() << std::endl;
498   }
499   else
500   {
501     std::string os_path = FileSystem::join(realdir, archive);
502
503     PHYSFS_addToSearchPath(os_path.c_str(), 0);
504
505     std::string nfo_filename = scan_for_info(os_path);
506
507     if (nfo_filename.empty())
508     {
509       log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
510     }
511     else
512     {
513       try
514       {
515         std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
516         addon->set_install_filename(os_path, md5);
517         m_installed_addons.push_back(std::move(addon));
518       }
519       catch (const std::runtime_error& e)
520       {
521         log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
522       }
523     }
524
525     PHYSFS_removeFromSearchPath(os_path.c_str());
526   }
527 }
528
529 void
530 AddonManager::add_installed_addons()
531 {
532   auto archives = scan_for_archives();
533
534   for(auto archive : archives)
535   {
536     MD5 md5 = md5_from_file(archive);
537     add_installed_archive(archive, md5.hex_digest());
538   }
539 }
540
541 AddonManager::AddonList
542 AddonManager::parse_addon_infos(const std::string& filename) const
543 {
544   AddonList m_addons;
545
546   try
547   {
548     lisp::Parser parser;
549     const lisp::Lisp* root = parser.parse(filename);
550     const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
551     if(!addons_lisp)
552     {
553       throw std::runtime_error("Downloaded file is not an Add-on list");
554     }
555     else
556     {
557       lisp::ListIterator iter(addons_lisp);
558       while(iter.next())
559       {
560         const std::string& token = iter.item();
561         if(token != "supertux-addoninfo")
562         {
563           log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
564         }
565         else
566         {
567           std::unique_ptr<Addon> addon = Addon::parse(*iter.lisp());
568           m_addons.push_back(std::move(addon));
569         }
570       }
571
572       return m_addons;
573     }
574   }
575   catch(const std::exception& e)
576   {
577     std::stringstream msg;
578     msg << "Problem when reading Add-on list: " << e.what();
579     throw std::runtime_error(msg.str());
580   }
581
582   return m_addons;
583 }
584
585 void
586 AddonManager::update()
587 {
588   m_downloader.update();
589 }
590
591 void
592 AddonManager::abort_install()
593 {
594   log_info << "addon install aborted" << std::endl;
595
596   m_downloader.abort(m_transfer_status->id);
597
598   m_install_request = {};
599   m_transfer_status = {};
600 }
601
602 /* EOF */