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