Files
simgear/simgear/package/Catalog.cxx
T

663 lines
18 KiB
C++

// Copyright (C) 2013 James Turner - zakalawe@mac.com
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Library General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library 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
// Library 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 <simgear_config.h>
#include <simgear/package/Catalog.hxx>
#include <algorithm>
#include <cassert>
#include <cstring>
#include <fstream>
#include <simgear/debug/logstream.hxx>
#include <simgear/props/props_io.hxx>
#include <simgear/io/HTTPRequest.hxx>
#include <simgear/io/HTTPClient.hxx>
#include <simgear/misc/sg_dir.hxx>
#include <simgear/io/iostreams/sgstream.hxx>
#include <simgear/structure/exception.hxx>
#include <simgear/package/Package.hxx>
#include <simgear/package/Root.hxx>
#include <simgear/package/Install.hxx>
#include <simgear/misc/strutils.hxx>
namespace simgear {
namespace pkg {
bool checkVersion(const std::string& aVersion, SGPropertyNode_ptr props)
{
for (SGPropertyNode* v : props->getChildren("version")) {
std::string s(v->getStringValue());
if (strutils::compareVersionToWildcard(aVersion, s)) {
return true;
}
}
return false;
}
SGPropertyNode_ptr alternateForVersion(const std::string& aVersion, SGPropertyNode_ptr props)
{
for (auto v : props->getChildren("alternate-version")) {
for (auto versionSpec : v->getChildren("version")) {
if (strutils::compareVersionToWildcard(aVersion, versionSpec->getStringValue())) {
return v;
}
}
}
return {};
}
std::string redirectUrlForVersion(const std::string& aVersion, SGPropertyNode_ptr props)
{
auto node = alternateForVersion(aVersion, props);
if (node)
return node->getStringValue("url");;
return {};
}
//////////////////////////////////////////////////////////////////////////////
class Catalog::Downloader : public HTTP::Request
{
public:
Downloader(CatalogRef aOwner, const std::string& aUrl) :
HTTP::Request(aUrl),
m_owner(aOwner)
{
// refreshing
m_owner->changeStatus(Delegate::STATUS_IN_PROGRESS);
}
protected:
void gotBodyData(const char* s, int n) override
{
m_buffer += std::string(s, n);
}
void onDone() override
{
if (responseCode() != 200) {
Delegate::StatusCode code = Delegate::FAIL_DOWNLOAD;
SG_LOG(SG_GENERAL, SG_ALERT, "catalog download failure:" << m_owner->url()
<< "\n\t" << responseCode());
if (responseCode() == 404) {
code = Delegate::FAIL_NOT_FOUND;
}
m_owner->refreshComplete(code);
return;
}
SGPropertyNode_ptr props = new SGPropertyNode;
try {
readProperties(m_buffer.data(), m_buffer.size(), props);
m_owner->parseProps(props);
} catch (sg_exception& ) {
SG_LOG(SG_GENERAL, SG_ALERT, "catalog parse failure:" << m_owner->url());
m_owner->refreshComplete(Delegate::FAIL_EXTRACT);
return;
}
std::string ver(m_owner->root()->applicationVersion());
if (!checkVersion(ver, props)) {
// check for a version redirect entry
auto alt = alternateForVersion(ver, props);
if (alt) {
SG_LOG(SG_GENERAL, SG_INFO, "have alternate version of package at:"
<< alt->getStringValue("url"));
m_owner->processAlternate(alt);
} else {
SG_LOG(SG_GENERAL, SG_DEBUG, "downloaded catalog " << m_owner->url()
<< ", but app version " << ver << " is not compatible");
m_owner->refreshComplete(Delegate::FAIL_VERSION);
}
return;
} // of version check failed
// validate what we downloaded, in case it's now corrupted
// (i.e someone uploaded bad XML data)
if (!m_owner->validatePackages()) {
m_owner->refreshComplete(Delegate::FAIL_VALIDATION);
return;
}
// cache the catalog data, now we have a valid install root
Dir d(m_owner->installRoot());
SGPath p = d.file("catalog.xml");
sg_ofstream f(p, std::ios::out | std::ios::trunc);
f.write(m_buffer.data(), m_buffer.size());
f.close();
time(&m_owner->m_retrievedTime);
m_owner->writeTimestamp();
m_owner->refreshComplete(Delegate::STATUS_REFRESHED);
}
void onFail() override
{
// network level failure
SG_LOG(SG_GENERAL, SG_WARN, "catalog network failure for:" << m_owner->url());
m_owner->refreshComplete(Delegate::FAIL_DOWNLOAD);
}
private:
CatalogRef m_owner;
std::string m_buffer;
};
//////////////////////////////////////////////////////////////////////////////
Catalog::Catalog(Root *aRoot) :
m_root(aRoot)
{
}
Catalog::~Catalog()
{
}
CatalogRef Catalog::createFromUrl(Root* aRoot, const std::string& aUrl)
{
CatalogRef c = new Catalog(aRoot);
c->m_url = aUrl;
c->refresh();
return c;
}
CatalogRef Catalog::createFromPath(Root* aRoot, const SGPath& aPath)
{
SGPath xml = aPath;
xml.append("catalog.xml");
if (!xml.exists()) {
return nullptr;
}
SGPropertyNode_ptr props;
try {
props = new SGPropertyNode;
readProperties(xml, props);
} catch (sg_exception& ) {
return nullptr;
}
bool versionCheckOk = checkVersion(aRoot->applicationVersion(), props);
if (!versionCheckOk) {
SG_LOG(SG_GENERAL, SG_INFO, "catalog at:" << aPath << " failed version check: app version: "
<< aRoot->applicationVersion());
// keep the catalog but mark it as needing an update
} else {
SG_LOG(SG_GENERAL, SG_DEBUG, "creating catalog from:" << aPath);
}
// check for the marker file we write, to mark a catalog as disabled
const SGPath disableMarkerFile = aPath / "_disabled_";
CatalogRef c = new Catalog(aRoot);
c->m_installRoot = aPath;
if (disableMarkerFile.exists()) {
c->m_userEnabled = false;
}
c->parseProps(props);
c->parseTimestamp();
if (!c->validatePackages()) {
c->changeStatus(Delegate::FAIL_VALIDATION);
} else if (!versionCheckOk) {
c->changeStatus(Delegate::FAIL_VERSION);
} else if (!c->m_userEnabled) {
c->changeStatus(Delegate::USER_DISABLED);
} else {
// parsed XML ok, mark status as valid
c->changeStatus(Delegate::STATUS_SUCCESS);
}
return c;
}
bool Catalog::validatePackages() const
{
for (auto pack : packages()) {
if (!pack->validate()) {
SG_LOG(SG_GENERAL, SG_WARN, "Catalog " << id() << " failed validation due to invalid package:" << pack->id());
return false;
}
}
return true;
}
bool Catalog::uninstall()
{
bool ok;
bool atLeastOneFailure = false;
try {
// clean uninstall of each airacft / package in turn. This is
// slightly overkill since we then nuke the entire catalog
// directory anyway
for (PackageRef p : installedPackages()) {
ok = p->existingInstall()->uninstall();
if (!ok) {
SG_LOG(SG_GENERAL, SG_WARN, "uninstall of package " <<
p->id() << " failed");
// continue trying other packages, bailing out here
// gains us nothing
atLeastOneFailure = true;
}
}
} catch (sg_exception& e) {
SG_LOG(SG_GENERAL, SG_WARN, "uninstall of catalog failed " << e.getMessage() << ", will clean-up directory");
atLeastOneFailure = true;
}
ok = removeDirectory();
if (!ok) {
atLeastOneFailure = true;
}
changeStatus(atLeastOneFailure ? Delegate::FAIL_FILESYSTEM
: Delegate::STATUS_SUCCESS);
return ok;
}
bool Catalog::removeDirectory()
{
Dir d(m_installRoot);
if (!m_installRoot.exists())
return true;
return d.remove(true /* recursive */);
}
PackageList const&
Catalog::packages() const
{
return m_packages;
}
PackageList
Catalog::packagesMatching(const SGPropertyNode* aFilter) const
{
PackageList r;
for (auto p : m_packages) {
if (p->matches(aFilter)) {
r.push_back(p);
}
}
return r;
}
PackageList
Catalog::packagesNeedingUpdate() const
{
PackageList r;
for (auto p : m_packages) {
if (!p->isInstalled()) {
continue;
}
if (p->install()->hasUpdate()) {
r.push_back(p);
}
}
return r;
}
PackageList
Catalog::installedPackages() const
{
PackageList r;
for (auto p : m_packages) {
if (p->isInstalled()) {
r.push_back(p);
}
}
return r;
}
void Catalog::refresh()
{
if (m_refreshRequest.valid()) {
// refresh in progress
return;
}
Downloader* dl = new Downloader(this, url());
m_refreshRequest = dl;
// will update status to IN_PROGRESS
m_root->makeHTTPRequest(dl);
}
struct FindById
{
FindById(const std::string &id) : m_id(id) {}
bool operator()(const PackageRef& ref) const
{
return ref->id() == m_id;
}
std::string m_id;
};
void Catalog::parseProps(const SGPropertyNode* aProps)
{
// copy everything except package children?
m_props = new SGPropertyNode;
m_variantDict.clear(); // will rebuild during parse
std::set<PackageRef> orphans;
orphans.insert(m_packages.begin(), m_packages.end());
int nChildren = aProps->nChildren();
for (int i = 0; i < nChildren; i++) {
const SGPropertyNode* pkgProps = aProps->getChild(i);
if (strcmp(pkgProps->getName(), "package") == 0) {
// can't use getPackageById here becuase the variant dict isn't
// built yet. Instead we need to look at m_packages directly.
PackageList::iterator pit = std::find_if(m_packages.begin(), m_packages.end(),
FindById(pkgProps->getStringValue("id")));
PackageRef p;
if (pit != m_packages.end()) {
p = *pit;
// existing package
p->updateFromProps(pkgProps);
orphans.erase(p); // not an orphan
} else {
// new package
p = new Package(pkgProps, this);
m_packages.push_back(p);
}
string_list vars(p->variants());
for (string_list::iterator it = vars.begin(); it != vars.end(); ++it) {
m_variantDict[*it] = p.ptr();
}
} else {
SGPropertyNode* c = m_props->getChild(pkgProps->getName(), pkgProps->getIndex(), true);
copyProperties(pkgProps, c);
}
} // of children iteration
if (!orphans.empty()) {
SG_LOG(SG_GENERAL, SG_WARN, "have orphan packages: will become inaccesible");
std::set<PackageRef>::iterator it;
for (it = orphans.begin(); it != orphans.end(); ++it) {
SG_LOG(SG_GENERAL, SG_WARN, "\torphan package:" << (*it)->qualifiedId());
PackageList::iterator pit = std::find(m_packages.begin(), m_packages.end(), *it);
assert(pit != m_packages.end());
m_packages.erase(pit);
}
}
if (!m_url.empty()) {
if (m_url != m_props->getStringValue("url")) {
// this effectively allows packages to migrate to new locations,
// although if we're going to rely on that feature we should
// maybe formalise it!
SG_LOG(SG_GENERAL, SG_WARN, "package downloaded from:" << m_url
<< " is now at: " << m_props->getStringValue("url"));
}
}
m_url = m_props->getStringValue("url");
if (m_installRoot.isNull()) {
m_installRoot = m_root->path();
m_installRoot.append(id());
Dir d(m_installRoot);
d.create(0755);
}
}
PackageRef Catalog::getPackageById(const std::string& aId) const
{
// search the variant dict here, so looking up aircraft variants
// works as expected.
PackageWeakMap::const_iterator it = m_variantDict.find(aId);
if (it == m_variantDict.end())
return PackageRef();
return it->second;
}
PackageRef Catalog::getPackageByPath(const std::string& aPath) const
{
PackageList::const_iterator it;
for (it = m_packages.begin(); it != m_packages.end(); ++it) {
if ((*it)->dirName() == aPath) {
return *it;
}
}
return PackageRef();
}
std::string Catalog::id() const
{
return m_props->getStringValue("id");
}
std::string Catalog::url() const
{
return m_url;
}
void Catalog::setUrl(const std::string &url)
{
m_url = url;
if (m_status == Delegate::FAIL_NOT_FOUND) {
m_status = Delegate::FAIL_UNKNOWN;
}
}
std::string Catalog::name() const
{
return getLocalisedString(m_props, "name");
}
std::string Catalog::description() const
{
return getLocalisedString(m_props, "description");
}
SGPropertyNode* Catalog::properties() const
{
return m_props.ptr();
}
void Catalog::parseTimestamp()
{
SGPath timestampFile = m_installRoot;
timestampFile.append(".timestamp");
sg_ifstream f(timestampFile, std::ios::in);
f >> m_retrievedTime;
}
void Catalog::writeTimestamp()
{
SGPath timestampFile = m_installRoot;
timestampFile.append(".timestamp");
sg_ofstream f(timestampFile, std::ios::out | std::ios::trunc);
f << m_retrievedTime << std::endl;
}
unsigned int Catalog::ageInSeconds() const
{
time_t now;
time(&now);
int diff = ::difftime(now, m_retrievedTime);
return (diff < 0) ? 0 : diff;
}
bool Catalog::needsRefresh() const
{
// always refresh in these cases
if ((m_status == Delegate::FAIL_VERSION) || (m_status == Delegate::FAIL_DOWNLOAD)) {
return true;
}
unsigned int maxAge = m_props->getIntValue("max-age-sec", m_root->maxAgeSeconds());
return (ageInSeconds() > maxAge);
}
std::string Catalog::getLocalisedString(const SGPropertyNode* aRoot, const char* aName) const
{
if (!aRoot) {
return std::string();
}
if (aRoot->hasChild(m_root->getLocale())) {
const SGPropertyNode* localeRoot = aRoot->getChild(m_root->getLocale().c_str());
if (localeRoot->hasChild(aName)) {
return localeRoot->getStringValue(aName);
}
}
return aRoot->getStringValue(aName);
}
void Catalog::refreshComplete(Delegate::StatusCode aReason)
{
m_refreshRequest.reset();
changeStatus(aReason);
}
void Catalog::changeStatus(Delegate::StatusCode newStatus)
{
if (m_status == newStatus) {
return;
}
m_status = newStatus;
m_root->catalogRefreshStatus(this, newStatus);
m_statusCallbacks(this);
}
void Catalog::addStatusCallback(const Callback& cb)
{
m_statusCallbacks.push_back(cb);
}
Delegate::StatusCode Catalog::status() const
{
return m_status;
}
bool Catalog::isEnabled() const
{
if (!m_userEnabled)
return false;
switch (m_status) {
case Delegate::STATUS_SUCCESS:
case Delegate::STATUS_REFRESHED:
case Delegate::STATUS_IN_PROGRESS:
// this is important so we can use Catalog aircraft in offline mode
case Delegate::FAIL_DOWNLOAD:
return true;
default:
return false;
}
}
bool Catalog::isUserEnabled() const
{
return m_userEnabled;
}
void Catalog::setUserEnabled(bool b)
{
if (m_userEnabled == b)
return;
m_userEnabled = b;
SGPath disableMarkerFile = installRoot() / "_disabled_";
if (m_userEnabled) {
sg_ofstream of(disableMarkerFile);
of << "1\n"; // touch the file
} else {
bool ok = disableMarkerFile.remove();
if (!ok) {
SG_LOG(SG_GENERAL, SG_ALERT, "Failed to remove catalog-disable marker file:" << disableMarkerFile);
}
}
Delegate::StatusCode effectiveStatus = m_status;
if ((m_status == Delegate::STATUS_SUCCESS) && !m_userEnabled) {
effectiveStatus = Delegate::USER_DISABLED;
}
m_root->catalogRefreshStatus(this, effectiveStatus);
}
void Catalog::processAlternate(SGPropertyNode_ptr alt)
{
std::string altId;
const auto idPtr = alt->getStringValue("id");
if (idPtr) {
altId = std::string(idPtr);
}
std::string altUrl;
if (alt->getStringValue("url")) {
altUrl = std::string(alt->getStringValue("url"));
}
CatalogRef existing;
if (!altId.empty()) {
existing = root()->getCatalogById(altId);
} else {
existing = root()->getCatalogByUrl(altUrl);
}
if (existing && (existing != this)) {
// we already have the alternate, so just go quiet here
changeStatus(Delegate::FAIL_VERSION);
return;
}
// we have an alternate ID, and it's differnt from our ID, so let's
// define a new catalog
if (!altId.empty()) {
SG_LOG(SG_GENERAL, SG_INFO, "Adding new catalog:" << altId << " as version alternate for " << id());
// new catalog being added
createFromUrl(root(), altUrl);
// and we can go idle now
changeStatus(Delegate::FAIL_VERSION);
return;
}
SG_LOG(SG_GENERAL, SG_INFO, "Migrating catalog " << id() << " to new URL:" << altUrl);
setUrl(altUrl);
Downloader* dl = new Downloader(this, altUrl);
root()->makeHTTPRequest(dl);
}
} // of namespace pkg
} // of namespace simgear