1 // SuperTux - Add-on Manager
2 // Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
3 // 2014 Ingo Ruhnke <grumbel@gmail.com>
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.
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.
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/>.
18 #include "addon/addon_manager.hpp"
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"
43 MD5 md5_from_file(const std::string& filename)
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();
51 unsigned char buffer[1024];
52 PHYSFS_file* file = PHYSFS_openRead(filename.c_str());
55 std::ostringstream out;
56 out << "PHYSFS_openRead() failed: " << PHYSFS_getLastError();
57 throw std::runtime_error(out.str());
63 PHYSFS_sint64 len = PHYSFS_read(file, buffer, 1, sizeof(buffer));
65 md5.update(buffer, len);
73 bool has_suffix(const std::string& str, const std::string& suffix)
75 if (str.length() >= suffix.length())
76 return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
83 AddonManager::AddonManager(const std::string& addon_directory,
84 std::vector<Config::Addon>& addon_config) :
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),
90 m_repository_addons(),
91 m_has_been_updated(false),
96 PHYSFS_mkdir(m_addon_directory.c_str());
98 add_installed_addons();
100 // FIXME: We should also restore the order here
101 for(auto& addon : m_addon_config)
107 enable_addon(addon.id);
109 catch(const std::exception& err)
111 log_warning << "failed to enable addon from config: " << err.what() << std::endl;
117 AddonManager::~AddonManager()
119 // sync enabled/disabled addons into the config for saving
120 m_addon_config.clear();
121 for(auto& addon : m_installed_addons)
123 m_addon_config.push_back({addon->get_id(), addon->is_enabled()});
128 AddonManager::get_repository_addon(const AddonId& id)
130 auto it = std::find_if(m_repository_addons.begin(), m_repository_addons.end(),
131 [&id](const std::unique_ptr<Addon>& addon)
133 return addon->get_id() == id;
136 if (it != m_repository_addons.end())
142 throw std::runtime_error("Couldn't find repository Addon with id: " + id);
147 AddonManager::get_installed_addon(const AddonId& id)
149 auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
150 [&id](const std::unique_ptr<Addon>& addon)
152 return addon->get_id() == id;
155 if (it != m_installed_addons.end())
161 throw std::runtime_error("Couldn't find installed Addon with id: " + id);
166 AddonManager::get_repository_addons() const
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)
174 return addon->get_id();
181 AddonManager::get_installed_addons() const
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)
189 return addon->get_id();
195 AddonManager::has_online_support() const
201 AddonManager::has_been_updated() const
203 return m_has_been_updated;
206 AddonManager::InstallStatusPtr
207 AddonManager::request_check_online()
209 if (m_install_status)
211 throw std::runtime_error("only one addon install request allowed at a time");
215 m_transfer_status = m_downloader.request_download(m_repository_url, "/addons/repository.nfo");
217 m_transfer_status->then([this]{
218 m_repository_addons = parse_addon_infos("/addons/repository.nfo");
219 m_has_been_updated = true;
221 if (m_install_status->callback)
223 m_install_status->callback();
226 m_install_status = {};
227 m_transfer_status = {};
230 m_install_status = std::make_shared<InstallStatus>();
231 return m_install_status;
236 AddonManager::check_online()
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;
243 AddonManager::InstallStatusPtr
244 AddonManager::request_install_addon(const AddonId& addon_id)
246 if (m_install_status)
248 throw std::runtime_error("only one addon install request allowed at a time");
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)
256 return addon->get_id() == addon_id;
258 if (it != m_installed_addons.end())
260 log_debug << "reinstalling addon " << addon_id << std::endl;
261 if ((*it)->is_enabled())
263 disable_addon((*it)->get_id());
265 m_installed_addons.erase(it);
269 log_debug << "installing addon " << addon_id << std::endl;
274 Addon& repository_addon = get_repository_addon(addon_id);
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;
280 m_transfer_status = m_downloader.request_download(repository_addon.get_url(),
281 m_install_request->install_filename);
284 m_transfer_status->then(
287 // complete the addon install
288 Addon& repository_addon = get_repository_addon(m_install_request->addon_id);
290 MD5 md5 = md5_from_file(m_install_request->install_filename);
291 if (repository_addon.get_md5() != md5.hex_digest())
293 if (PHYSFS_delete(m_install_request->install_filename.c_str()) == 0)
295 log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
298 throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
302 const char* realdir = PHYSFS_getRealDir(m_install_request->install_filename.c_str());
305 throw std::runtime_error("PHYSFS_getRealDir failed: " + m_install_request->install_filename);
309 add_installed_archive(m_install_request->install_filename, md5.hex_digest());
313 // signal that the request is done and cleanup
314 if (m_install_status->callback)
316 m_install_status->callback();
319 m_install_request = {};
320 m_install_status = {};
321 m_transfer_status = {};
324 m_install_status = std::make_shared<InstallStatus>();
326 return m_install_status;
331 AddonManager::install_addon(const AddonId& addon_id)
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)
337 return addon->get_id() == addon_id;
339 if (it != m_installed_addons.end())
341 log_debug << "reinstalling addon " << addon_id << std::endl;
342 if ((*it)->is_enabled())
344 disable_addon((*it)->get_id());
346 m_installed_addons.erase(it);
350 log_debug << "installing addon " << addon_id << std::endl;
354 Addon& repository_addon = get_repository_addon(addon_id);
356 std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
358 m_downloader.download(repository_addon.get_url(), install_filename);
360 MD5 md5 = md5_from_file(install_filename);
361 if (repository_addon.get_md5() != md5.hex_digest())
363 if (PHYSFS_delete(install_filename.c_str()) == 0)
365 log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
368 throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
372 const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
375 throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
379 add_installed_archive(install_filename, md5.hex_digest());
385 AddonManager::uninstall_addon(const AddonId& addon_id)
387 log_debug << "uninstalling addon " << addon_id << std::endl;
388 Addon& addon = get_installed_addon(addon_id);
389 if (addon.is_enabled())
391 disable_addon(addon_id);
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)
398 return addon.get_id() == rhs->get_id();
400 m_installed_addons.end());
404 AddonManager::enable_addon(const AddonId& addon_id)
406 log_debug << "enabling addon " << addon_id << std::endl;
407 Addon& addon = get_installed_addon(addon_id);
408 if (addon.is_enabled())
410 log_warning << "Tried enabling already enabled Add-on" << std::endl;
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)
418 log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
419 << PHYSFS_getLastError() << std::endl;
423 addon.set_enabled(true);
429 AddonManager::disable_addon(const AddonId& addon_id)
431 log_debug << "disabling addon " << addon_id << std::endl;
432 Addon& addon = get_installed_addon(addon_id);
433 if (!addon.is_enabled())
435 log_warning << "Tried disabling already disabled Add-On" << std::endl;
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)
442 log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
443 << PHYSFS_getLastError() << std::endl;
447 addon.set_enabled(false);
452 std::vector<std::string>
453 AddonManager::scan_for_archives() const
455 std::vector<std::string> archives;
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()),
461 for(char** i = rc.get(); *i != 0; ++i)
463 if (has_suffix(*i, ".zip"))
465 std::string archive = FileSystem::join(m_addon_directory, *i);
466 if (PHYSFS_exists(archive.c_str()))
468 archives.push_back(archive);
477 AddonManager::scan_for_info(const std::string& archive_os_path) const
479 std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
480 rc2(PHYSFS_enumerateFiles("/"),
482 for(char** j = rc2.get(); *j != 0; ++j)
484 if (has_suffix(*j, ".nfo"))
486 std::string nfo_filename = FileSystem::join("/", *j);
488 // make sure it's in the current archive_os_path
489 const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
492 log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastError() << std::endl;
496 if (realdir == archive_os_path)
504 return std::string();
508 AddonManager::add_installed_archive(const std::string& archive, const std::string& md5)
510 const char* realdir = PHYSFS_getRealDir(archive.c_str());
513 log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
514 << PHYSFS_getLastError() << std::endl;
518 std::string os_path = FileSystem::join(realdir, archive);
520 PHYSFS_addToSearchPath(os_path.c_str(), 0);
522 std::string nfo_filename = scan_for_info(os_path);
524 if (nfo_filename.empty())
526 log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
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));
536 catch (const std::runtime_error& e)
538 log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
542 PHYSFS_removeFromSearchPath(os_path.c_str());
547 AddonManager::add_installed_addons()
549 auto archives = scan_for_archives();
551 for(auto archive : archives)
553 MD5 md5 = md5_from_file(archive);
554 add_installed_archive(archive, md5.hex_digest());
558 AddonManager::AddonList
559 AddonManager::parse_addon_infos(const std::string& filename) const
566 const lisp::Lisp* root = parser.parse(filename);
567 const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
570 throw std::runtime_error("Downloaded file is not an Add-on list");
574 lisp::ListIterator iter(addons_lisp);
577 const std::string& token = iter.item();
578 if(token != "supertux-addoninfo")
580 log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
584 std::unique_ptr<Addon> addon = Addon::parse(*iter.lisp());
585 m_addons.push_back(std::move(addon));
592 catch(const std::exception& e)
594 std::stringstream msg;
595 msg << "Problem when reading Add-on list: " << e.what();
596 throw std::runtime_error(msg.str());
603 AddonManager::update()
605 m_downloader.update();
607 if (m_install_status)
609 m_install_status->now = m_transfer_status->dlnow;
610 m_install_status->total = m_transfer_status->dltotal;
615 AddonManager::abort_install()
617 log_info << "addon install aborted" << std::endl;
619 m_downloader.abort(m_transfer_status->id);
621 m_install_request = {};
622 m_install_status = {};
623 m_transfer_status = {};