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