1 // Copyright (C) 2013 James Turner - zakalawe@mac.com
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Library General Public
5 // License as published by the Free Software Foundation; either
6 // version 2 of the License, or (at your option) any later version.
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 // Library General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with this program; if not, write to the Free Software
15 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 #include <simgear/package/Catalog.hxx>
20 #include <boost/foreach.hpp>
25 #include <simgear/debug/logstream.hxx>
26 #include <simgear/props/props_io.hxx>
27 #include <simgear/io/HTTPRequest.hxx>
28 #include <simgear/io/HTTPClient.hxx>
29 #include <simgear/misc/sg_dir.hxx>
30 #include <simgear/structure/exception.hxx>
31 #include <simgear/package/Package.hxx>
32 #include <simgear/package/Root.hxx>
33 #include <simgear/package/Install.hxx>
39 bool checkVersion(const std::string& aVersion, SGPropertyNode_ptr props)
41 BOOST_FOREACH(SGPropertyNode* v, props->getChildren("version")) {
42 std::string s(v->getStringValue());
47 // allow 3.5.* to match any of 3.5.0, 3.5.1rc1, 3.5.11 or so on
48 if (strutils::ends_with(s, ".*")) {
49 size_t lastDot = aVersion.rfind('.');
50 std::string ver = aVersion.substr(0, lastDot);
51 if (ver == s.substr(0, s.length() - 2)) {
59 std::string redirectUrlForVersion(const std::string& aVersion, SGPropertyNode_ptr props)
61 BOOST_FOREACH(SGPropertyNode* v, props->getChildren("alternate-version")) {
62 if (v->getStringValue("version") == aVersion) {
63 return v->getStringValue("url");
70 //////////////////////////////////////////////////////////////////////////////
72 class Catalog::Downloader : public HTTP::Request
75 Downloader(CatalogRef aOwner, const std::string& aUrl) :
80 m_owner->changeStatus(Delegate::FAIL_IN_PROGRESS);
85 virtual void gotBodyData(const char* s, int n)
87 m_buffer += std::string(s, n);
92 if (responseCode() != 200) {
93 SG_LOG(SG_GENERAL, SG_ALERT, "catalog download failure:" << m_owner->url());
94 m_owner->refreshComplete(Delegate::FAIL_DOWNLOAD);
98 SGPropertyNode* props = new SGPropertyNode;
101 readProperties(m_buffer.data(), m_buffer.size(), props);
102 m_owner->parseProps(props);
103 } catch (sg_exception& e) {
104 SG_LOG(SG_GENERAL, SG_ALERT, "catalog parse failure:" << m_owner->url());
105 m_owner->refreshComplete(Delegate::FAIL_EXTRACT);
109 std::string ver(m_owner->root()->catalogVersion());
110 if (!checkVersion(ver, props)) {
111 SG_LOG(SG_GENERAL, SG_WARN, "downloaded catalog " << m_owner->url() << ", version mismatch:\n\t"
112 << props->getStringValue("version") << " vs required " << ver);
114 // check for a version redirect entry
115 std::string url = redirectUrlForVersion(ver, props);
117 SG_LOG(SG_GENERAL, SG_WARN, "redirecting from " << m_owner->url() <<
120 // update the URL and kick off a new request
121 m_owner->m_url = url;
122 Downloader* dl = new Downloader(m_owner, url);
123 m_owner->root()->makeHTTPRequest(dl);
125 m_owner->refreshComplete(Delegate::FAIL_VERSION);
129 } // of version check failed
131 // cache the catalog data, now we have a valid install root
132 Dir d(m_owner->installRoot());
133 SGPath p = d.file("catalog.xml");
135 std::ofstream f(p.c_str(), std::ios::out | std::ios::trunc);
136 f.write(m_buffer.data(), m_buffer.size());
139 time(&m_owner->m_retrievedTime);
140 m_owner->writeTimestamp();
141 m_owner->refreshComplete(Delegate::CATALOG_REFRESHED);
147 std::string m_buffer;
150 //////////////////////////////////////////////////////////////////////////////
152 Catalog::Catalog(Root *aRoot) :
154 m_status(Delegate::FAIL_UNKNOWN),
163 CatalogRef Catalog::createFromUrl(Root* aRoot, const std::string& aUrl)
165 CatalogRef c = new Catalog(aRoot);
167 Downloader* dl = new Downloader(c, aUrl);
168 aRoot->makeHTTPRequest(dl);
173 CatalogRef Catalog::createFromPath(Root* aRoot, const SGPath& aPath)
176 xml.append("catalog.xml");
181 SGPropertyNode_ptr props;
183 props = new SGPropertyNode;
184 readProperties(xml.str(), props);
185 } catch (sg_exception& e) {
189 if (!checkVersion(aRoot->catalogVersion(), props)) {
190 std::string redirect = redirectUrlForVersion(aRoot->catalogVersion(), props);
191 if (!redirect.empty()) {
192 SG_LOG(SG_GENERAL, SG_WARN, "catalog at " << aPath << ", version mismatch:\n\t"
193 << "redirecting to alternate URL:" << redirect);
194 CatalogRef c = Catalog::createFromUrl(aRoot, redirect);
195 c->m_installRoot = aPath;
198 SG_LOG(SG_GENERAL, SG_WARN, "skipping catalog at " << aPath << ", version mismatch:\n\t"
199 << props->getStringValue("version") << " vs required " << aRoot->catalogVersion());
205 CatalogRef c = new Catalog(aRoot);
206 c->m_installRoot = aPath;
207 c->parseProps(props); // will set status
213 bool Catalog::uninstall()
216 bool atLeastOneFailure = false;
218 BOOST_FOREACH(PackageRef p, installedPackages()) {
219 ok = p->existingInstall()->uninstall();
221 SG_LOG(SG_GENERAL, SG_WARN, "uninstall of package " <<
222 p->id() << " failed");
223 // continue trying other packages, bailing out here
225 atLeastOneFailure = true;
229 Dir d(m_installRoot);
230 ok = d.remove(true /* recursive */);
232 atLeastOneFailure = true;
235 changeStatus(atLeastOneFailure ? Delegate::FAIL_FILESYSTEM
236 : Delegate::FAIL_SUCCESS);
241 Catalog::packages() const
247 Catalog::packagesMatching(const SGPropertyNode* aFilter) const
250 BOOST_FOREACH(PackageRef p, m_packages) {
251 if (p->matches(aFilter)) {
259 Catalog::packagesNeedingUpdate() const
262 BOOST_FOREACH(PackageRef p, m_packages) {
263 if (!p->isInstalled()) {
267 if (p->install()->hasUpdate()) {
275 Catalog::installedPackages() const
278 BOOST_FOREACH(PackageRef p, m_packages) {
279 if (p->isInstalled()) {
286 InstallRef Catalog::installForPackage(PackageRef pkg) const
288 PackageInstallDict::const_iterator it = m_installed.find(pkg);
289 if (it == m_installed.end()) {
290 // check if it exists on disk, create
292 SGPath p(pkg->pathOnDisk());
294 return Install::createFromPath(p, CatalogRef(const_cast<Catalog*>(this)));
303 void Catalog::refresh()
305 Downloader* dl = new Downloader(this, url());
306 // will iupdate status to IN_PROGRESS
307 m_root->makeHTTPRequest(dl);
308 m_root->catalogRefreshBegin(this);
311 void Catalog::parseProps(const SGPropertyNode* aProps)
313 // copy everything except package children?
314 m_props = new SGPropertyNode;
316 m_variantDict.clear(); // will rebuild during parse
317 std::set<PackageRef> orphans;
318 orphans.insert(m_packages.begin(), m_packages.end());
320 int nChildren = aProps->nChildren();
321 for (int i = 0; i < nChildren; i++) {
322 const SGPropertyNode* pkgProps = aProps->getChild(i);
323 if (strcmp(pkgProps->getName(), "package") == 0) {
324 PackageRef p = getPackageById(pkgProps->getStringValue("id"));
327 p->updateFromProps(pkgProps);
328 orphans.erase(p); // not an orphan
331 p = new Package(pkgProps, this);
332 m_packages.push_back(p);
335 string_list vars(p->variants());
336 for (string_list::iterator it = vars.begin(); it != vars.end(); ++it) {
337 m_variantDict[*it] = p.ptr();
340 SGPropertyNode* c = m_props->getChild(pkgProps->getName(), pkgProps->getIndex(), true);
341 copyProperties(pkgProps, c);
343 } // of children iteration
345 if (!orphans.empty()) {
346 SG_LOG(SG_GENERAL, SG_WARN, "have orphan packages: will become inaccesible");
347 std::set<PackageRef>::iterator it;
348 for (it = orphans.begin(); it != orphans.end(); ++it) {
349 SG_LOG(SG_GENERAL, SG_WARN, "\torphan package:" << (*it)->qualifiedId());
350 PackageList::iterator pit = std::find(m_packages.begin(), m_packages.end(), *it);
351 assert(pit != m_packages.end());
352 m_packages.erase(pit);
356 if (!m_url.empty()) {
357 if (m_url != m_props->getStringValue("url")) {
358 // this effectively allows packages to migrate to new locations,
359 // although if we're going to rely on that feature we should
360 // maybe formalise it!
361 SG_LOG(SG_GENERAL, SG_WARN, "package downloaded from:" << m_url
362 << " is now at: " << m_props->getStringValue("url"));
366 m_url = m_props->getStringValue("url");
368 if (m_installRoot.isNull()) {
369 m_installRoot = m_root->path();
370 m_installRoot.append(id());
372 Dir d(m_installRoot);
376 // parsed XML ok, mark status as valid
377 changeStatus(Delegate::FAIL_SUCCESS);
380 PackageRef Catalog::getPackageById(const std::string& aId) const
382 // search the variant dict here, so looking up aircraft variants
383 // works as expected.
384 PackageWeakMap::const_iterator it = m_variantDict.find(aId);
385 if (it == m_variantDict.end())
391 std::string Catalog::id() const
393 return m_props->getStringValue("id");
396 std::string Catalog::url() const
401 std::string Catalog::description() const
403 return getLocalisedString(m_props, "description");
406 SGPropertyNode* Catalog::properties() const
408 return m_props.ptr();
411 void Catalog::parseTimestamp()
413 SGPath timestampFile = m_installRoot;
414 timestampFile.append(".timestamp");
415 std::ifstream f(timestampFile.c_str(), std::ios::in);
416 f >> m_retrievedTime;
419 void Catalog::writeTimestamp()
421 SGPath timestampFile = m_installRoot;
422 timestampFile.append(".timestamp");
423 std::ofstream f(timestampFile.c_str(), std::ios::out | std::ios::trunc);
424 f << m_retrievedTime << std::endl;
427 unsigned int Catalog::ageInSeconds() const
431 int diff = ::difftime(now, m_retrievedTime);
432 return (diff < 0) ? 0 : diff;
435 bool Catalog::needsRefresh() const
437 unsigned int maxAge = m_props->getIntValue("max-age-sec", m_root->maxAgeSeconds());
438 return (ageInSeconds() > maxAge);
441 std::string Catalog::getLocalisedString(const SGPropertyNode* aRoot, const char* aName) const
443 if (aRoot->hasChild(m_root->getLocale())) {
444 const SGPropertyNode* localeRoot = aRoot->getChild(m_root->getLocale().c_str());
445 if (localeRoot->hasChild(aName)) {
446 return localeRoot->getStringValue(aName);
450 return aRoot->getStringValue(aName);
453 void Catalog::refreshComplete(Delegate::FailureCode aReason)
455 m_root->catalogRefreshComplete(this, aReason);
456 changeStatus(aReason);
459 void Catalog::registerInstall(Install* ins)
461 if (!ins || ins->package()->catalog() != this) {
465 m_installed[ins->package()] = ins;
468 void Catalog::unregisterInstall(Install* ins)
470 if (!ins || ins->package()->catalog() != this) {
474 m_installed.erase(ins->package());
477 void Catalog::changeStatus(Delegate::FailureCode newStatus)
479 if (m_status == newStatus) {
483 m_status = newStatus;
484 m_statusCallbacks(this);
487 void Catalog::addStatusCallback(const Callback& cb)
489 m_statusCallbacks.push_back(cb);
492 Delegate::FailureCode Catalog::status() const
497 } // of namespace pkg
499 } // of namespace simgear