]> git.mxchange.org Git - simgear.git/blob - simgear/package/Catalog.cxx
More permissive catalog version checks
[simgear.git] / simgear / package / Catalog.cxx
1 // Copyright (C) 2013  James Turner - zakalawe@mac.com
2 //
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.
7 //
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.
12 //
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.
16 //
17
18 #include <simgear/package/Catalog.hxx>
19
20 #include <boost/foreach.hpp>
21 #include <algorithm>
22 #include <fstream>
23 #include <cstring>
24
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>
34 #include <simgear/misc/strutils.hxx>
35
36 namespace simgear {
37
38 namespace pkg {
39
40 bool checkVersionString(const std::string& aVersion, const std::string& aCandidate)
41 {
42     if (aCandidate == aVersion) {
43         return true;
44     }
45
46     // examine each dot-seperated component in turn, supporting a wildcard
47     // in the versions from the catalog.
48     string_list appVersionParts = simgear::strutils::split(aVersion, ".");
49     string_list catVersionParts = simgear::strutils::split(aCandidate, ".");
50
51     size_t partCount = appVersionParts.size();
52     bool previousCandidatePartWasWildcard = false;
53
54     for (unsigned int p=0; p < partCount; ++p) {
55         // candidate string is too short, can match if it ended with wildcard
56         // part. This allows candidate '2016.*' to match '2016.1.2' and so on
57         if (catVersionParts.size() <= p) {
58             return previousCandidatePartWasWildcard;
59         }
60
61         if (catVersionParts[p] == "*") {
62             // always passes
63             previousCandidatePartWasWildcard = true;
64         } else if (appVersionParts[p] != catVersionParts[p]) {
65             return false;
66         }
67     }
68
69     return true;
70 }
71
72 bool checkVersion(const std::string& aVersion, SGPropertyNode_ptr props)
73 {
74     BOOST_FOREACH(SGPropertyNode* v, props->getChildren("version")) {
75         std::string s(v->getStringValue());
76         if (checkVersionString(aVersion, s)) {
77             return true;
78         }
79     }
80     return false;
81 }
82
83 std::string redirectUrlForVersion(const std::string& aVersion, SGPropertyNode_ptr props)
84 {
85     BOOST_FOREACH(SGPropertyNode* v, props->getChildren("alternate-version")) {
86         std::string s(v->getStringValue("version"));
87         if (checkVersionString(aVersion, s)) {
88             return  v->getStringValue("url");;
89         }
90     }
91
92     return std::string();
93 }
94
95 //////////////////////////////////////////////////////////////////////////////
96
97 class Catalog::Downloader : public HTTP::Request
98 {
99 public:
100     Downloader(CatalogRef aOwner, const std::string& aUrl) :
101         HTTP::Request(aUrl),
102         m_owner(aOwner)
103     {
104         // refreshing
105         m_owner->changeStatus(Delegate::STATUS_IN_PROGRESS);
106     }
107
108 protected:
109     virtual void gotBodyData(const char* s, int n)
110     {
111         m_buffer += std::string(s, n);
112     }
113
114     virtual void onDone()
115     {
116         if (responseCode() != 200) {
117             Delegate::StatusCode code = Delegate::FAIL_DOWNLOAD;
118             SG_LOG(SG_GENERAL, SG_ALERT, "catalog download failure:" << m_owner->url()
119                    << "\n\t" << responseCode());
120             if (responseCode() == 404) {
121                 code = Delegate::FAIL_NOT_FOUND;
122             }
123             m_owner->refreshComplete(code);
124             return;
125         }
126
127         SGPropertyNode* props = new SGPropertyNode;
128
129         try {
130             readProperties(m_buffer.data(), m_buffer.size(), props);
131             m_owner->parseProps(props);
132         } catch (sg_exception& ) {
133             SG_LOG(SG_GENERAL, SG_ALERT, "catalog parse failure:" << m_owner->url());
134             m_owner->refreshComplete(Delegate::FAIL_EXTRACT);
135             return;
136         }
137
138         std::string ver(m_owner->root()->applicationVersion());
139         if (!checkVersion(ver, props)) {
140             SG_LOG(SG_GENERAL, SG_WARN, "downloaded catalog " << m_owner->url() << ", version required " << ver);
141
142             // check for a version redirect entry
143             std::string url = redirectUrlForVersion(ver, props);
144             if (!url.empty()) {
145                 SG_LOG(SG_GENERAL, SG_WARN, "redirecting from " << m_owner->url() <<
146                        " to \n\t" << url);
147
148                 // update the URL and kick off a new request
149                 m_owner->setUrl(url);
150                 Downloader* dl = new Downloader(m_owner, url);
151                 m_owner->root()->makeHTTPRequest(dl);
152             } else {
153                 m_owner->refreshComplete(Delegate::FAIL_VERSION);
154             }
155
156             return;
157         } // of version check failed
158
159         // cache the catalog data, now we have a valid install root
160         Dir d(m_owner->installRoot());
161         SGPath p = d.file("catalog.xml");
162
163         std::ofstream f(p.c_str(), std::ios::out | std::ios::trunc);
164         f.write(m_buffer.data(), m_buffer.size());
165         f.close();
166
167         time(&m_owner->m_retrievedTime);
168         m_owner->writeTimestamp();
169         m_owner->refreshComplete(Delegate::STATUS_REFRESHED);
170     }
171
172 private:
173
174     CatalogRef m_owner;
175     std::string m_buffer;
176 };
177
178 //////////////////////////////////////////////////////////////////////////////
179
180 Catalog::Catalog(Root *aRoot) :
181     m_root(aRoot),
182     m_status(Delegate::FAIL_UNKNOWN),
183     m_retrievedTime(0)
184 {
185 }
186
187 Catalog::~Catalog()
188 {
189 }
190
191 CatalogRef Catalog::createFromUrl(Root* aRoot, const std::string& aUrl)
192 {
193     CatalogRef c = new Catalog(aRoot);
194     c->m_url = aUrl;
195     c->refresh();
196     return c;
197 }
198
199 CatalogRef Catalog::createFromPath(Root* aRoot, const SGPath& aPath)
200 {
201     SGPath xml = aPath;
202     xml.append("catalog.xml");
203     if (!xml.exists()) {
204         return NULL;
205     }
206
207     SGPropertyNode_ptr props;
208     try {
209         props = new SGPropertyNode;
210         readProperties(xml.str(), props);
211     } catch (sg_exception& ) {
212         return NULL;
213     }
214
215     bool versionCheckOk = checkVersion(aRoot->applicationVersion(), props);
216     if (!versionCheckOk) {
217         SG_LOG(SG_GENERAL, SG_INFO, "catalog at:" << aPath << " failed version check: need" << aRoot->applicationVersion());
218         // keep the catalog but mark it as needing an update
219     } else {
220         SG_LOG(SG_GENERAL, SG_DEBUG, "creating catalog from:" << aPath);
221     }
222
223     CatalogRef c = new Catalog(aRoot);
224     c->m_installRoot = aPath;
225     c->parseProps(props);
226     c->parseTimestamp();
227
228     if (versionCheckOk) {
229         // parsed XML ok, mark status as valid
230         c->changeStatus(Delegate::STATUS_SUCCESS);
231     } else {
232         c->changeStatus(Delegate::FAIL_VERSION);
233     }
234
235     return c;
236 }
237
238 bool Catalog::uninstall()
239 {
240     bool ok;
241     bool atLeastOneFailure = false;
242
243     BOOST_FOREACH(PackageRef p, installedPackages()) {
244         ok = p->existingInstall()->uninstall();
245         if (!ok) {
246             SG_LOG(SG_GENERAL, SG_WARN, "uninstall of package " <<
247                 p->id() << " failed");
248             // continue trying other packages, bailing out here
249             // gains us nothing
250             atLeastOneFailure = true;
251         }
252     }
253
254     Dir d(m_installRoot);
255     ok = d.remove(true /* recursive */);
256     if (!ok) {
257         atLeastOneFailure = true;
258     }
259
260     changeStatus(atLeastOneFailure ? Delegate::FAIL_FILESYSTEM
261                                    : Delegate::STATUS_SUCCESS);
262     return ok;
263 }
264
265 PackageList const&
266 Catalog::packages() const
267 {
268   return m_packages;
269 }
270
271 PackageList
272 Catalog::packagesMatching(const SGPropertyNode* aFilter) const
273 {
274     PackageList r;
275     BOOST_FOREACH(PackageRef p, m_packages) {
276         if (p->matches(aFilter)) {
277             r.push_back(p);
278         }
279     }
280     return r;
281 }
282
283 PackageList
284 Catalog::packagesNeedingUpdate() const
285 {
286     PackageList r;
287     BOOST_FOREACH(PackageRef p, m_packages) {
288         if (!p->isInstalled()) {
289             continue;
290         }
291
292         if (p->install()->hasUpdate()) {
293             r.push_back(p);
294         }
295     }
296     return r;
297 }
298
299 PackageList
300 Catalog::installedPackages() const
301 {
302   PackageList r;
303   BOOST_FOREACH(PackageRef p, m_packages) {
304     if (p->isInstalled()) {
305       r.push_back(p);
306     }
307   }
308   return r;
309 }
310
311 void Catalog::refresh()
312 {
313     if (m_refreshRequest.valid()) {
314         // refresh in progress
315         return;
316     }
317
318     Downloader* dl = new Downloader(this, url());
319     m_refreshRequest = dl;
320     // will update status to IN_PROGRESS
321     m_root->makeHTTPRequest(dl);
322 }
323
324 struct FindById
325 {
326     FindById(const std::string &id) : m_id(id) {}
327
328     bool operator()(const PackageRef& ref) const
329     {
330         return ref->id() == m_id;
331     }
332
333     std::string m_id;
334 };
335
336 void Catalog::parseProps(const SGPropertyNode* aProps)
337 {
338     // copy everything except package children?
339     m_props = new SGPropertyNode;
340
341     m_variantDict.clear(); // will rebuild during parse
342     std::set<PackageRef> orphans;
343     orphans.insert(m_packages.begin(), m_packages.end());
344
345     int nChildren = aProps->nChildren();
346     for (int i = 0; i < nChildren; i++) {
347         const SGPropertyNode* pkgProps = aProps->getChild(i);
348         if (strcmp(pkgProps->getName(), "package") == 0) {
349             // can't use getPackageById here becuase the variant dict isn't
350             // built yet. Instead we need to look at m_packages directly.
351
352             PackageList::iterator pit = std::find_if(m_packages.begin(), m_packages.end(),
353                                                   FindById(pkgProps->getStringValue("id")));
354             PackageRef p;
355             if (pit != m_packages.end()) {
356                 p = *pit;
357                 // existing package
358                 p->updateFromProps(pkgProps);
359                 orphans.erase(p); // not an orphan
360             } else {
361                 // new package
362                 p = new Package(pkgProps, this);
363                 m_packages.push_back(p);
364             }
365
366             string_list vars(p->variants());
367             for (string_list::iterator it = vars.begin(); it != vars.end(); ++it) {
368                 m_variantDict[*it] = p.ptr();
369             }
370         } else {
371             SGPropertyNode* c = m_props->getChild(pkgProps->getName(), pkgProps->getIndex(), true);
372             copyProperties(pkgProps, c);
373         }
374     } // of children iteration
375
376     if (!orphans.empty()) {
377         SG_LOG(SG_GENERAL, SG_WARN, "have orphan packages: will become inaccesible");
378         std::set<PackageRef>::iterator it;
379         for (it = orphans.begin(); it != orphans.end(); ++it) {
380             SG_LOG(SG_GENERAL, SG_WARN, "\torphan package:" << (*it)->qualifiedId());
381             PackageList::iterator pit = std::find(m_packages.begin(), m_packages.end(), *it);
382             assert(pit != m_packages.end());
383             m_packages.erase(pit);
384         }
385     }
386
387     if (!m_url.empty()) {
388         if (m_url != m_props->getStringValue("url")) {
389             // this effectively allows packages to migrate to new locations,
390             // although if we're going to rely on that feature we should
391             // maybe formalise it!
392             SG_LOG(SG_GENERAL, SG_WARN, "package downloaded from:" << m_url
393                    << " is now at: " << m_props->getStringValue("url"));
394         }
395     }
396
397     m_url = m_props->getStringValue("url");
398
399     if (m_installRoot.isNull()) {
400         m_installRoot = m_root->path();
401         m_installRoot.append(id());
402
403         Dir d(m_installRoot);
404         d.create(0755);
405     }
406 }
407
408 PackageRef Catalog::getPackageById(const std::string& aId) const
409 {
410     // search the variant dict here, so looking up aircraft variants
411     // works as expected.
412     PackageWeakMap::const_iterator it = m_variantDict.find(aId);
413     if (it == m_variantDict.end())
414         return PackageRef();
415
416     return it->second;
417 }
418
419 PackageRef Catalog::getPackageByPath(const std::string& aPath) const
420 {
421     PackageList::const_iterator it;
422     for (it = m_packages.begin(); it != m_packages.end(); ++it) {
423         if ((*it)->dirName() == aPath) {
424             return *it;
425         }
426     }
427
428     return PackageRef();
429 }
430
431 std::string Catalog::id() const
432 {
433     return m_props->getStringValue("id");
434 }
435
436 std::string Catalog::url() const
437 {
438     return m_url;
439 }
440
441 void Catalog::setUrl(const std::string &url)
442 {
443     m_url = url;
444     if (m_status == Delegate::FAIL_NOT_FOUND) {
445         m_status = Delegate::FAIL_UNKNOWN;
446     }
447 }
448
449 std::string Catalog::name() const
450 {
451     return getLocalisedString(m_props, "name");
452 }
453
454 std::string Catalog::description() const
455 {
456     return getLocalisedString(m_props, "description");
457 }
458
459 SGPropertyNode* Catalog::properties() const
460 {
461     return m_props.ptr();
462 }
463
464 void Catalog::parseTimestamp()
465 {
466     SGPath timestampFile = m_installRoot;
467     timestampFile.append(".timestamp");
468     std::ifstream f(timestampFile.c_str(), std::ios::in);
469     f >> m_retrievedTime;
470 }
471
472 void Catalog::writeTimestamp()
473 {
474     SGPath timestampFile = m_installRoot;
475     timestampFile.append(".timestamp");
476     std::ofstream f(timestampFile.c_str(), std::ios::out | std::ios::trunc);
477     f << m_retrievedTime << std::endl;
478 }
479
480 unsigned int Catalog::ageInSeconds() const
481 {
482     time_t now;
483     time(&now);
484     int diff = ::difftime(now, m_retrievedTime);
485     return (diff < 0) ? 0 : diff;
486 }
487
488 bool Catalog::needsRefresh() const
489 {
490     // always refresh in these cases
491     if ((m_status == Delegate::FAIL_VERSION) || (m_status == Delegate::FAIL_DOWNLOAD)) {
492         return true;
493     }
494
495     unsigned int maxAge = m_props->getIntValue("max-age-sec", m_root->maxAgeSeconds());
496     return (ageInSeconds() > maxAge);
497 }
498
499 std::string Catalog::getLocalisedString(const SGPropertyNode* aRoot, const char* aName) const
500 {
501     if (!aRoot) {
502         return std::string();
503     }
504
505     if (aRoot->hasChild(m_root->getLocale())) {
506         const SGPropertyNode* localeRoot = aRoot->getChild(m_root->getLocale().c_str());
507         if (localeRoot->hasChild(aName)) {
508             return localeRoot->getStringValue(aName);
509         }
510     }
511
512     return aRoot->getStringValue(aName);
513 }
514
515 void Catalog::refreshComplete(Delegate::StatusCode aReason)
516 {
517     m_refreshRequest.reset();
518     changeStatus(aReason);
519 }
520
521 void Catalog::changeStatus(Delegate::StatusCode newStatus)
522 {
523     if (m_status == newStatus) {
524         return;
525     }
526
527     m_status = newStatus;
528     m_root->catalogRefreshStatus(this, newStatus);
529     m_statusCallbacks(this);
530 }
531
532 void Catalog::addStatusCallback(const Callback& cb)
533 {
534     m_statusCallbacks.push_back(cb);
535 }
536
537 Delegate::StatusCode Catalog::status() const
538 {
539     return m_status;
540 }
541
542 } // of namespace pkg
543
544 } // of namespace simgear