297 lines
8.8 KiB
C++
297 lines
8.8 KiB
C++
// -*- coding: utf-8 -*-
|
|
//
|
|
// AddonManager.cxx --- Manager class for FlightGear add-ons
|
|
// Copyright (C) 2017 Florent Rougon
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation; either version 2 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along
|
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
#include "config.h"
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <cstdlib>
|
|
#include <cassert>
|
|
|
|
#include <simgear/debug/logstream.hxx>
|
|
#include <simgear/misc/sg_path.hxx>
|
|
#include <simgear/misc/strutils.hxx>
|
|
#include <simgear/props/props.hxx>
|
|
#include <simgear/props/props_io.hxx>
|
|
#include <simgear/structure/exception.hxx>
|
|
|
|
#include <Main/fg_props.hxx>
|
|
#include <Main/globals.hxx>
|
|
|
|
#include "addon_fwd.hxx"
|
|
#include "Addon.hxx"
|
|
#include "AddonManager.hxx"
|
|
#include "AddonVersion.hxx"
|
|
#include "exceptions.hxx"
|
|
#include "pointer_traits.hxx"
|
|
|
|
namespace strutils = simgear::strutils;
|
|
|
|
using std::string;
|
|
using std::vector;
|
|
using std::shared_ptr;
|
|
using std::unique_ptr;
|
|
|
|
namespace flightgear
|
|
{
|
|
|
|
namespace addons
|
|
{
|
|
|
|
static unique_ptr<AddonManager> staticInstance;
|
|
|
|
// ***************************************************************************
|
|
// * AddonManager *
|
|
// ***************************************************************************
|
|
|
|
// Static method
|
|
const unique_ptr<AddonManager>&
|
|
AddonManager::createInstance()
|
|
{
|
|
SG_LOG(SG_GENERAL, SG_DEBUG, "Initializing the AddonManager");
|
|
|
|
staticInstance.reset(new AddonManager());
|
|
return staticInstance;
|
|
}
|
|
|
|
// Static method
|
|
const unique_ptr<AddonManager>&
|
|
AddonManager::instance()
|
|
{
|
|
return staticInstance;
|
|
}
|
|
|
|
// Static method
|
|
void
|
|
AddonManager::reset()
|
|
{
|
|
SG_LOG(SG_GENERAL, SG_DEBUG, "Resetting the AddonManager");
|
|
staticInstance.reset();
|
|
}
|
|
|
|
// Static method
|
|
void
|
|
AddonManager::loadConfigFileIfExists(const SGPath& configFile)
|
|
{
|
|
if (!configFile.exists()) {
|
|
return;
|
|
}
|
|
|
|
SGPropertyNode_ptr configProps(new SGPropertyNode);
|
|
try {
|
|
readProperties(configFile, configProps);
|
|
} catch (const sg_exception &e) {
|
|
throw errors::error_loading_config_file(
|
|
"unable to load add-on config file '" + configFile.utf8Str() + "': " +
|
|
e.getFormattedMessage());
|
|
}
|
|
|
|
// bug https://sourceforge.net/p/flightgear/codetickets/2059/
|
|
// since we're loading this after autosave.xml is loaded, the defaults
|
|
// always take precedence. To fix this, only copy a value from the
|
|
// addon-config if it's not makred as ARCHIVE. (We assume ARCHIVE props
|
|
// came from autosave.xml)
|
|
copyPropertiesIf(configProps, globals->get_props(), [](const SGPropertyNode* src) {
|
|
if (src->nChildren() > 0)
|
|
return true;
|
|
|
|
// find the correspnding destination node
|
|
auto dstNode = globals->get_props()->getNode(src->getPath());
|
|
if (!dstNode)
|
|
return true; // easy, just copy it
|
|
|
|
// copy if it's NOT marked archive. In other words, we can replace
|
|
// values from defaults, but not autosave
|
|
return dstNode->getAttribute(SGPropertyNode::USERARCHIVE) == false;
|
|
});
|
|
|
|
SG_LOG(SG_GENERAL, SG_INFO,
|
|
"Loaded add-on config file: '" << configFile.utf8Str() + "'");
|
|
}
|
|
|
|
string
|
|
AddonManager::registerAddonMetadata(const SGPath& addonPath)
|
|
{
|
|
using ptr_traits = shared_ptr_traits<AddonRef>;
|
|
AddonRef addon = ptr_traits::makeStrongRef(Addon::fromAddonDir(addonPath));
|
|
string addonId = addon->getId();
|
|
|
|
SGPropertyNode* addonPropNode = fgGetNode("addons", true)
|
|
->getChild("by-id", 0, 1)
|
|
->getChild(addonId, 0, 1);
|
|
addon->setAddonNode(addonPropNode);
|
|
addon->setLoadSequenceNumber(_loadSequenceNumber++);
|
|
|
|
// Check that the FlightGear version satisfies the add-on requirements
|
|
std::string minFGversion = addon->getMinFGVersionRequired();
|
|
if (strutils::compare_versions(FLIGHTGEAR_VERSION, minFGversion) < 0) {
|
|
throw errors::fg_version_too_old(
|
|
"add-on '" + addonId + "' requires FlightGear " + minFGversion +
|
|
" or later, however this is FlightGear " + FLIGHTGEAR_VERSION);
|
|
}
|
|
|
|
std::string maxFGversion = addon->getMaxFGVersionRequired();
|
|
if (maxFGversion != "none" &&
|
|
strutils::compare_versions(FLIGHTGEAR_VERSION, maxFGversion) > 0) {
|
|
throw errors::fg_version_too_recent(
|
|
"add-on '" + addonId + "' requires FlightGear " + maxFGversion +
|
|
" or earlier, however this is FlightGear " + FLIGHTGEAR_VERSION);
|
|
}
|
|
|
|
// Store the add-on metadata in _idToAddonMap
|
|
auto emplaceRetval = _idToAddonMap.emplace(addonId, std::move(addon));
|
|
|
|
// Prevent registration of two add-ons with the same id
|
|
if (!emplaceRetval.second) {
|
|
auto existingElt = _idToAddonMap.find(addonId);
|
|
assert(existingElt != _idToAddonMap.end());
|
|
throw errors::duplicate_registration_attempt(
|
|
"attempt to register add-on '" + addonId + "' with base path '"
|
|
+ addonPath.utf8Str() + "', however it is already registered with base "
|
|
"path '" + existingElt->second->getBasePath().utf8Str() + "'");
|
|
}
|
|
|
|
return addonId;
|
|
}
|
|
|
|
string
|
|
AddonManager::registerAddon(const SGPath& addonPath)
|
|
{
|
|
// Use realpath() as in FGGlobals::append_aircraft_path(), otherwise
|
|
// SGPath::validate() will deny access to resources under the add-on path
|
|
// if one of its components is a symlink.
|
|
const SGPath addonRealPath = addonPath.realpath();
|
|
const string addonId = registerAddonMetadata(addonRealPath);
|
|
loadConfigFileIfExists(addonRealPath / "addon-config.xml");
|
|
globals->append_aircraft_path(addonRealPath);
|
|
|
|
AddonRef addon{getAddon(addonId)};
|
|
addon->getLoadedFlagNode()->setBoolValue(false);
|
|
SGPropertyNode_ptr addonNode = addon->getAddonNode();
|
|
|
|
// Set a few properties for the add-on under this node
|
|
addonNode->getNode("id", true)->setStringValue(addonId);
|
|
addonNode->getNode("name", true)->setStringValue(addon->getName());
|
|
addonNode->getNode("version", true)
|
|
->setStringValue(addonVersion(addonId)->str());
|
|
addonNode->getNode("path", true)->setStringValue(addonRealPath.utf8Str());
|
|
addonNode->getNode("load-seq-num", true)
|
|
->setIntValue(addon->getLoadSequenceNumber());
|
|
|
|
// “Legacy node”. Should we remove these two lines?
|
|
SGPropertyNode* seqNumNode = fgGetNode("addons", true)->addChild("addon");
|
|
seqNumNode->getNode("path", true)->setStringValue(addonRealPath.utf8Str());
|
|
|
|
string msg = "Registered add-on '" + addon->getName() + "' (" + addonId +
|
|
") version " + addonVersion(addonId)->str() + "; "
|
|
"base path is '" + addonRealPath.utf8Str() + "'";
|
|
|
|
auto dataPath = addonRealPath / "FGData";
|
|
if (dataPath.exists()) {
|
|
SG_LOG(SG_GENERAL, SG_INFO, "Registering data path for add-on: " << addon->getName());
|
|
globals->append_data_path(dataPath, true /* after FG_ROOT */);
|
|
}
|
|
|
|
// This preserves the registration order
|
|
_registeredAddons.push_back(addon);
|
|
SG_LOG(SG_GENERAL, SG_INFO, msg);
|
|
|
|
return addonId;
|
|
}
|
|
|
|
bool
|
|
AddonManager::isAddonRegistered(const string& addonId) const
|
|
{
|
|
return (_idToAddonMap.find(addonId) != _idToAddonMap.end());
|
|
}
|
|
|
|
bool
|
|
AddonManager::isAddonLoaded(const string& addonId) const
|
|
{
|
|
return (isAddonRegistered(addonId) &&
|
|
getAddon(addonId)->getLoadedFlagNode()->getBoolValue());
|
|
}
|
|
|
|
vector<AddonRef>
|
|
AddonManager::registeredAddons() const
|
|
{
|
|
return _registeredAddons;
|
|
}
|
|
|
|
vector<AddonRef>
|
|
AddonManager::loadedAddons() const
|
|
{
|
|
vector<AddonRef> v;
|
|
v.reserve(_idToAddonMap.size()); // will be the right size most of the times
|
|
|
|
for (const auto& elem: _idToAddonMap) {
|
|
if (isAddonLoaded(elem.first)) {
|
|
v.push_back(elem.second);
|
|
}
|
|
}
|
|
|
|
return v;
|
|
}
|
|
|
|
AddonRef
|
|
AddonManager::getAddon(const string& addonId) const
|
|
{
|
|
const auto it = _idToAddonMap.find(addonId);
|
|
|
|
if (it == _idToAddonMap.end()) {
|
|
throw sg_exception("tried to get add-on '" + addonId + "', however no "
|
|
"such add-on has been registered.");
|
|
}
|
|
|
|
return it->second;
|
|
}
|
|
|
|
AddonVersionRef
|
|
AddonManager::addonVersion(const string& addonId) const
|
|
{
|
|
return getAddon(addonId)->getVersion();
|
|
}
|
|
|
|
SGPath
|
|
AddonManager::addonBasePath(const string& addonId) const
|
|
{
|
|
return getAddon(addonId)->getBasePath();
|
|
}
|
|
|
|
SGPropertyNode_ptr AddonManager::addonNode(const string& addonId) const
|
|
{
|
|
return getAddon(addonId)->getAddonNode();
|
|
}
|
|
|
|
void
|
|
AddonManager::addAddonMenusToFGMenubar() const
|
|
{
|
|
for (const auto& addon: _registeredAddons) {
|
|
addon->addToFGMenubar();
|
|
}
|
|
}
|
|
|
|
} // of namespace addons
|
|
|
|
} // of namespace flightgear
|