]> git.mxchange.org Git - simgear.git/blob - simgear/scene/tsync/terrasync.cxx
Fix SVN server fallback when query fails
[simgear.git] / simgear / scene / tsync / terrasync.cxx
1 // terrasync.cxx -- scenery fetcher
2 //
3 // Started by Curtis Olson, November 2002.
4 //
5 // Copyright (C) 2002  Curtis L. Olson  - http://www.flightgear.org/~curt
6 // Copyright (C) 2008  Alexander R. Perry <alex.perry@ieee.org>
7 // Copyright (C) 2011  Thorsten Brehm <brehmt@gmail.com>
8 //
9 // This program is free software; you can redistribute it and/or
10 // modify it under the terms of the GNU General Public License as
11 // published by the Free Software Foundation; either version 2 of the
12 // License, or (at your option) any later version.
13 //
14 // This program is distributed in the hope that it will be useful, but
15 // WITHOUT ANY WARRANTY; without even the implied warranty of
16 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17 // General Public License for more details.
18 //
19 // You should have received a copy of the GNU General Public License
20 // along with this program; if not, write to the Free Software
21 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
22 //
23 // $Id$
24
25 #ifdef HAVE_CONFIG_H
26 #  include <simgear_config.h>
27 #endif
28
29 #include <simgear/compiler.h>
30
31 #ifdef HAVE_WINDOWS_H
32 #include <windows.h>
33 #endif
34
35 #ifdef __MINGW32__
36 #  include <time.h>
37 #  include <unistd.h>
38 #elif defined(_MSC_VER)
39 #   include <io.h>
40 #   include <time.h>
41 #   include <process.h>
42 #endif
43
44 #include <stdlib.h>             // atoi() atof() abs() system()
45 #include <signal.h>             // signal()
46 #include <string.h>
47
48 #include <iostream>
49 #include <fstream>
50 #include <string>
51 #include <map>
52
53 #include <simgear/compiler.h>
54
55 #include "terrasync.hxx"
56
57 #include <simgear/bucket/newbucket.hxx>
58 #include <simgear/misc/sg_path.hxx>
59 #include <simgear/misc/strutils.hxx>
60 #include <simgear/threads/SGQueue.hxx>
61 #include <simgear/misc/sg_dir.hxx>
62 #include <simgear/debug/BufferedLogCallback.hxx>
63 #include <simgear/props/props_io.hxx>
64 #include <simgear/io/HTTPClient.hxx>
65 #include <simgear/io/SVNRepository.hxx>
66 #include <simgear/structure/exception.hxx>
67
68 static const bool svn_built_in_available = true;
69
70 using namespace simgear;
71 using namespace std;
72
73 const char* rsync_cmd =
74         "rsync --verbose --archive --delete --perms --owner --group";
75
76 const char* svn_options =
77         "checkout -q";
78
79 namespace UpdateInterval
80 {
81     // interval in seconds to allow an update to repeat after a successful update (=daily)
82     static const double SuccessfulAttempt = 24*60*60;
83     // interval in seconds to allow another update after a failed attempt (10 minutes)
84     static const double FailedAttempt     = 10*60;
85 }
86
87 typedef map<string,time_t> TileAgeCache;
88
89 ///////////////////////////////////////////////////////////////////////////////
90 // helper functions ///////////////////////////////////////////////////////////
91 ///////////////////////////////////////////////////////////////////////////////
92 string stripPath(string path)
93 {
94     // svn doesn't like trailing white-spaces or path separators - strip them!
95     path = simgear::strutils::strip(path);
96     size_t slen = path.length();
97     while ((slen>0)&&
98             ((path[slen-1]=='/')||(path[slen-1]=='\\')))
99     {
100         slen--;
101     }
102     return path.substr(0,slen);
103 }
104
105 bool hasWhitespace(string path)
106 {
107     return path.find(' ')!=string::npos;
108 }
109
110 ///////////////////////////////////////////////////////////////////////////////
111 // SyncItem ////////////////////////////////////////////////////////////
112 ///////////////////////////////////////////////////////////////////////////////
113 class SyncItem
114 {
115 public:
116     enum Type
117     {
118         Stop = 0,   ///< special item indicating to stop the SVNThread
119         Tile,
120         AirportData,
121         SharedModels,
122         AIData
123     };
124
125     enum Status
126     {
127         Invalid = 0,
128         Waiting,
129         Cached, ///< using already cached result
130         Updated,
131         NotFound,
132         Failed
133     };
134
135     SyncItem() :
136         _dir(),
137         _type(Stop),
138         _status(Invalid)
139     {
140     }
141
142     SyncItem(string dir, Type ty) :
143         _dir(dir),
144         _type(ty),
145         _status(Waiting)
146     {}
147
148     string _dir;
149     Type _type;
150     Status _status;
151 };
152
153 ///////////////////////////////////////////////////////////////////////////////
154
155 /**
156  * @brief SyncSlot encapsulates a queue of sync items we will fetch
157  * serially. Multiple slots exist to sync different types of item in
158  * parallel.
159  */
160 class SyncSlot
161 {
162 public:
163     SyncSlot() :
164         isNewDirectory(false),
165         busy(false)
166     {}
167
168     SyncItem currentItem;
169     bool isNewDirectory;
170     std::queue<SyncItem> queue;
171     std::auto_ptr<SVNRepository> repository;
172     SGTimeStamp stamp;
173     bool busy; ///< is the slot working or idle
174
175     unsigned int nextWarnTimeout;
176 };
177
178 static const int SYNC_SLOT_TILES = 0; ///< Terrain and Objects sync
179 static const int SYNC_SLOT_SHARED_DATA = 1; /// shared Models and Airport data
180 static const int SYNC_SLOT_AI_DATA = 2; /// AI traffic and models
181 static const unsigned int NUM_SYNC_SLOTS = 3;
182
183 /**
184  * @brief translate a sync item type into one of the available slots.
185  * This provides the scheduling / balancing / prioritising between slots.
186  */
187 static unsigned int syncSlotForType(SyncItem::Type ty)
188 {
189     switch (ty) {
190     case SyncItem::Tile: return SYNC_SLOT_TILES;
191     case SyncItem::SharedModels:
192     case SyncItem::AirportData:
193         return SYNC_SLOT_SHARED_DATA;
194     case SyncItem::AIData:
195         return SYNC_SLOT_AI_DATA;
196
197     default:
198         return SYNC_SLOT_SHARED_DATA;
199     }
200 }
201
202 ///////////////////////////////////////////////////////////////////////////////
203 // Base server query
204 ///////////////////////////////////////////////////////////////////////////////
205
206 class ServerSelectQuery : public HTTP::Request
207 {
208 public:
209     ServerSelectQuery() :
210         HTTP::Request("http://scenery.flightgear.org/svn-server", "GET")
211     {
212     }
213
214     std::string svnUrl() const
215     {
216         std::string s = simgear::strutils::strip(m_url);
217         if (s.at(s.length() - 1) == '/') {
218             s = s.substr(0, s.length() - 1);
219         }
220
221         return s;
222     }
223 protected:
224     virtual void gotBodyData(const char* s, int n)
225     {
226         m_url.append(std::string(s, n));
227     }
228
229     virtual void onFail()
230     {
231         SG_LOG(SG_TERRAIN, SG_ALERT, "Failed to query TerraSync SVN server");
232         HTTP::Request::onFail();
233     }
234
235 private:
236     std::string m_url;
237 };
238
239
240 ///////////////////////////////////////////////////////////////////////////////
241 // SGTerraSync::SvnThread /////////////////////////////////////////////////////
242 ///////////////////////////////////////////////////////////////////////////////
243 class SGTerraSync::SvnThread : public SGThread
244 {
245 public:
246    SvnThread();
247    virtual ~SvnThread( ) { stop(); }
248
249    void stop();
250    bool start();
251
252     bool isIdle() {return !_busy; }
253    void request(const SyncItem& dir) {waitingTiles.push_front(dir);}
254    bool isDirty() { bool r = _is_dirty;_is_dirty = false;return r;}
255    bool hasNewTiles() { return !_freshTiles.empty();}
256    SyncItem getNewTile() { return _freshTiles.pop_front();}
257
258    void   setSvnServer(string server)       { _svn_server   = stripPath(server);}
259    void   setSvnDataServer(string server)   { _svn_data_server   = stripPath(server);}
260
261    void   setExtSvnUtility(string svn_util) { _svn_command  = simgear::strutils::strip(svn_util);}
262    void   setRsyncServer(string server)     { _rsync_server = simgear::strutils::strip(server);}
263    void   setLocalDir(string dir)           { _local_dir    = stripPath(dir);}
264    string getLocalDir()                     { return _local_dir;}
265    void   setUseSvn(bool use_svn)           { _use_svn = use_svn;}
266    void   setAllowedErrorCount(int errors)  {_allowed_errors = errors;}
267
268    void   setCachePath(const SGPath& p)     {_persistentCachePath = p;}
269    void   setCacheHits(unsigned int hits)   {_cache_hits = hits;}
270    void   setUseBuiltin(bool built_in) { _use_built_in = built_in;}
271
272    volatile bool _active;
273    volatile bool _running;
274    volatile bool _busy;
275    volatile bool _stalled;
276    volatile int  _fail_count;
277    volatile int  _updated_tile_count;
278    volatile int  _success_count;
279    volatile int  _consecutive_errors;
280    volatile int  _allowed_errors;
281    volatile int  _cache_hits;
282    volatile int _transfer_rate;
283    // kbytes, not bytes, because bytes might overflow 2^31
284    volatile int _total_kb_downloaded;
285
286 private:
287    virtual void run();
288
289     // external model run and helpers
290     void runExternal();
291     void syncPathExternal(const SyncItem& next);
292     bool runExternalSyncCommand(const char* dir);
293
294     // internal mode run and helpers
295     void runInternal();
296     void updateSyncSlot(SyncSlot& slot);
297
298     // commond helpers between both internal and external models
299
300     SyncItem::Status isPathCached(const SyncItem& next) const;
301     void initCompletedTilesPersistentCache();
302     void writeCompletedTilesPersistentCache() const;
303     void updated(SyncItem item, bool isNewDirectory);
304     void fail(SyncItem failedItem);
305     void notFound(SyncItem notFoundItem);
306
307     bool _use_built_in;
308     HTTP::Client _http;
309     SyncSlot _syncSlots[NUM_SYNC_SLOTS];
310
311    volatile bool _is_dirty;
312    volatile bool _stop;
313    SGBlockingDeque <SyncItem> waitingTiles;
314
315    TileAgeCache _completedTiles;
316    TileAgeCache _notFoundItems;
317
318    SGBlockingDeque <SyncItem> _freshTiles;
319    bool _use_svn;
320    string _svn_server;
321    string _svn_data_server;
322    string _svn_command;
323    string _rsync_server;
324    string _local_dir;
325    SGPath _persistentCachePath;
326 };
327
328 SGTerraSync::SvnThread::SvnThread() :
329     _active(false),
330     _running(false),
331     _busy(false),
332     _stalled(false),
333     _fail_count(0),
334     _updated_tile_count(0),
335     _success_count(0),
336     _consecutive_errors(0),
337     _allowed_errors(6),
338     _cache_hits(0),
339     _transfer_rate(0),
340     _total_kb_downloaded(0),
341     _use_built_in(true),
342     _is_dirty(false),
343     _stop(false),
344     _use_svn(true)
345 {
346     _http.setUserAgent("terrascenery-" SG_STRINGIZE(SG_VERSION));
347 }
348
349 void SGTerraSync::SvnThread::stop()
350 {
351     // drop any pending requests
352     waitingTiles.clear();
353
354     if (!_running)
355         return;
356
357     // set stop flag and wake up the thread with an empty request
358     _stop = true;
359     SyncItem w(string(), SyncItem::Stop);
360     request(w);
361     join();
362     _running = false;
363 }
364
365 bool SGTerraSync::SvnThread::start()
366 {
367     if (_running)
368         return false;
369
370     if (_local_dir=="")
371     {
372         SG_LOG(SG_TERRAIN,SG_ALERT,
373                "Cannot start scenery download. Local cache directory is undefined.");
374         _fail_count++;
375         _stalled = true;
376         return false;
377     }
378
379     SGPath path(_local_dir);
380     if (!path.exists())
381     {
382         SG_LOG(SG_TERRAIN,SG_ALERT,
383                "Cannot start scenery download. Directory '" << _local_dir <<
384                "' does not exist. Set correct directory path or create directory folder.");
385         _fail_count++;
386         _stalled = true;
387         return false;
388     }
389
390     path.append("version");
391     if (path.exists())
392     {
393         SG_LOG(SG_TERRAIN,SG_ALERT,
394                "Cannot start scenery download. Directory '" << _local_dir <<
395                "' contains the base package. Use a separate directory.");
396         _fail_count++;
397         _stalled = true;
398         return false;
399     }
400
401     _use_svn |= _use_built_in;
402
403
404     if ((!_use_svn)&&(_rsync_server==""))
405     {
406         SG_LOG(SG_TERRAIN,SG_ALERT,
407                "Cannot start scenery download. Rsync scenery server is undefined.");
408         _fail_count++;
409         _stalled = true;
410         return false;
411     }
412
413     _fail_count = 0;
414     _updated_tile_count = 0;
415     _success_count = 0;
416     _consecutive_errors = 0;
417     _stop = false;
418     _stalled = false;
419     _running = true;
420
421     string status;
422
423     if (_use_svn && _use_built_in)
424         status = "Using built-in SVN support. ";
425     else if (_use_svn)
426     {
427         status = "Using external SVN utility '";
428         status += _svn_command;
429         status += "'. ";
430     }
431     else
432     {
433         status = "Using RSYNC. ";
434     }
435
436     // not really an alert - but we want to (always) see this message, so user is
437     // aware we're downloading scenery (and using bandwidth).
438     SG_LOG(SG_TERRAIN,SG_ALERT,
439            "Starting automatic scenery download/synchronization. "
440            << status
441            << "Directory: '" << _local_dir << "'.");
442
443     SGThread::start();
444     return true;
445 }
446
447 bool SGTerraSync::SvnThread::runExternalSyncCommand(const char* dir)
448 {
449     ostringstream buf;
450     SGPath localPath( _local_dir );
451     localPath.append( dir );
452
453     if (_use_svn)
454     {
455         buf << "\"" << _svn_command << "\" "
456             << svn_options << " "
457             << "\"" << _svn_server << "/" << dir << "\" "
458             << "\"" << localPath.str_native() << "\"";
459     } else {
460         buf << rsync_cmd << " "
461             << "\"" << _rsync_server << "/" << dir << "/\" "
462             << "\"" << localPath.str_native() << "/\"";
463     }
464
465     string command;
466 #ifdef SG_WINDOWS
467         // windows command line parsing is just lovely...
468         // to allow white spaces, the system call needs this:
469         // ""C:\Program Files\something.exe" somearg "some other arg""
470         // Note: whitespace strings quoted by a pair of "" _and_ the
471         //       entire string needs to be wrapped by "" too.
472         // The svn url needs forward slashes (/) as a path separator while
473         // the local path needs windows-native backslash as a path separator.
474     command = "\"" + buf.str() + "\"";
475 #else
476     command = buf.str();
477 #endif
478     SG_LOG(SG_TERRAIN,SG_DEBUG, "sync command '" << command << "'");
479
480 #ifdef SG_WINDOWS
481     // tbd: does Windows support "popen"?
482     int rc = system( command.c_str() );
483 #else
484     FILE* pipe = popen( command.c_str(), "r");
485     int rc=-1;
486     // wait for external process to finish
487     if (pipe)
488         rc = pclose(pipe);
489 #endif
490
491     if (rc)
492     {
493         SG_LOG(SG_TERRAIN,SG_ALERT,
494                "Failed to synchronize directory '" << dir << "', " <<
495                "error code= " << rc);
496         return false;
497     }
498     return true;
499 }
500
501 void SGTerraSync::SvnThread::run()
502 {
503     _active = true;
504     initCompletedTilesPersistentCache();
505
506     {
507         if (_svn_server.empty()) {
508             SG_LOG(SG_TERRAIN,SG_INFO, "Querying closest TerraSync server");
509             ServerSelectQuery* ssq = new ServerSelectQuery;
510             HTTP::Request_ptr req = ssq;
511             _http.makeRequest(req);
512             while (!req->isComplete()) {
513                 _http.update(20);
514             }
515
516             if (req->readyState() == HTTP::Request::DONE) {
517                 _svn_server = ssq->svnUrl();
518                 SG_LOG(SG_TERRAIN,SG_INFO, "Closest TerraSync server:" << _svn_server);
519             } else {
520                 SG_LOG(SG_TERRAIN,SG_WARN, "Failed to query closest TerraSync server");
521             }
522         } else {
523             SG_LOG(SG_TERRAIN,SG_INFO, "Explicit: TerraSync server:" << _svn_server);
524         }
525
526         if (_svn_server.empty()) {
527             // default value
528             _svn_server = "http://foxtrot.mgras.net:8080/terrascenery/trunk/data/Scenery";
529         }
530     }
531
532     if (_use_built_in) {
533         runInternal();
534     } else {
535         runExternal();
536     }
537
538     _active = false;
539     _running = false;
540     _is_dirty = true;
541 }
542
543 void SGTerraSync::SvnThread::runExternal()
544 {
545     while (!_stop)
546     {
547         SyncItem next = waitingTiles.pop_front();
548         if (_stop)
549            break;
550
551         SyncItem::Status cacheStatus = isPathCached(next);
552         if (cacheStatus != SyncItem::Invalid) {
553             _cache_hits++;
554             SG_LOG(SG_TERRAIN, SG_DEBUG,
555                    "Cache hit for: '" << next._dir << "'");
556             next._status = cacheStatus;
557             _freshTiles.push_back(next);
558             _is_dirty = true;
559             continue;
560         }
561
562         syncPathExternal(next);
563
564         if ((_allowed_errors >= 0)&&
565             (_consecutive_errors >= _allowed_errors))
566         {
567             _stalled = true;
568             _stop = true;
569         }
570     } // of thread running loop
571 }
572
573 void SGTerraSync::SvnThread::syncPathExternal(const SyncItem& next)
574 {
575     _busy = true;
576     SGPath path( _local_dir );
577     path.append( next._dir );
578     bool isNewDirectory = !path.exists();
579
580     try {
581         if (isNewDirectory) {
582             int rc = path.create_dir( 0755 );
583             if (rc) {
584                 SG_LOG(SG_TERRAIN,SG_ALERT,
585                        "Cannot create directory '" << path << "', return code = " << rc );
586                 throw sg_exception("Cannot create directory for terrasync", path.str());
587             }
588         }
589
590         if (!runExternalSyncCommand(next._dir.c_str())) {
591             throw sg_exception("Running external sync command failed");
592         }
593     } catch (sg_exception& e) {
594         fail(next);
595         _busy = false;
596         return;
597     }
598
599     updated(next, isNewDirectory);
600     _busy = false;
601 }
602
603 void SGTerraSync::SvnThread::updateSyncSlot(SyncSlot &slot)
604 {
605     if (slot.repository.get()) {
606         if (slot.repository->isDoingSync()) {
607 #if 1
608             if (slot.stamp.elapsedMSec() > slot.nextWarnTimeout) {
609                 SG_LOG(SG_TERRAIN, SG_INFO, "sync taking a long time:" << slot.currentItem._dir << " taken " << slot.stamp.elapsedMSec());
610                 SG_LOG(SG_TERRAIN, SG_INFO, "HTTP status:" << _http.hasActiveRequests());
611                 slot.nextWarnTimeout += 10000;
612             }
613 #endif
614             return; // easy, still working
615         }
616
617         // check result
618         SVNRepository::ResultCode res = slot.repository->failure();
619         if (res == SVNRepository::SVN_ERROR_NOT_FOUND) {
620             notFound(slot.currentItem);
621         } else if (res != SVNRepository::SVN_NO_ERROR) {
622             fail(slot.currentItem);
623         } else {
624             updated(slot.currentItem, slot.isNewDirectory);
625             SG_LOG(SG_TERRAIN, SG_DEBUG, "sync of " << slot.repository->baseUrl() << " finished ("
626                    << slot.stamp.elapsedMSec() << " msec");
627         }
628
629         // whatever happened, we're done with this repository instance
630         slot.busy = false;
631         slot.repository.reset();
632     }
633
634     // init and start sync of the next repository
635     if (!slot.queue.empty()) {
636         slot.currentItem = slot.queue.front();
637         slot.queue.pop();
638
639         SGPath path(_local_dir);
640         path.append(slot.currentItem._dir);
641         slot.isNewDirectory = !path.exists();
642         if (slot.isNewDirectory) {
643             int rc = path.create_dir( 0755 );
644             if (rc) {
645                 SG_LOG(SG_TERRAIN,SG_ALERT,
646                        "Cannot create directory '" << path << "', return code = " << rc );
647                 fail(slot.currentItem);
648                 return;
649             }
650         } // of creating directory step
651
652         string serverUrl(_svn_server);
653         if (slot.currentItem._type == SyncItem::AIData) {
654             serverUrl = _svn_data_server;
655         }
656
657         slot.repository.reset(new SVNRepository(path, &_http));
658         slot.repository->setBaseUrl(serverUrl + "/" + slot.currentItem._dir);
659         slot.repository->update();
660
661         slot.nextWarnTimeout = 20000;
662         slot.stamp.stamp();
663         slot.busy = true;
664         SG_LOG(SG_TERRAIN, SG_INFO, "sync of " << slot.repository->baseUrl() << " started, queue size is " << slot.queue.size());
665     }
666 }
667
668 void SGTerraSync::SvnThread::runInternal()
669 {
670     while (!_stop) {
671         _http.update(100);
672         _transfer_rate = _http.transferRateBytesPerSec();
673         // convert from bytes to kbytes
674         _total_kb_downloaded = static_cast<int>(_http.totalBytesDownloaded() / 1024);
675
676         if (_stop)
677             break;
678
679         // drain the waiting tiles queue into the sync slot queues.
680         while (!waitingTiles.empty()) {
681             SyncItem next = waitingTiles.pop_front();
682             SyncItem::Status cacheStatus = isPathCached(next);
683             if (cacheStatus != SyncItem::Invalid) {
684                 _cache_hits++;
685                 SG_LOG(SG_TERRAIN, SG_DEBUG, "\nTerraSync Cache hit for: '" << next._dir << "'");
686                 next._status = cacheStatus;
687                 _freshTiles.push_back(next);
688                 _is_dirty = true;
689                 continue;
690             }
691
692             unsigned int slot = syncSlotForType(next._type);
693             _syncSlots[slot].queue.push(next);
694         }
695
696         bool anySlotBusy = false;
697         // update each sync slot in turn
698         for (unsigned int slot=0; slot < NUM_SYNC_SLOTS; ++slot) {
699             updateSyncSlot(_syncSlots[slot]);
700             anySlotBusy |= _syncSlots[slot].busy;
701         }
702
703         _busy = anySlotBusy;
704         if (!anySlotBusy) {
705             // wait on the blocking deque here, otherwise we spin
706             // the loop very fast, since _http::update with no connections
707             // active returns immediately.
708             waitingTiles.waitOnNotEmpty();
709         }
710     } // of thread running loop
711 }
712
713 SyncItem::Status SGTerraSync::SvnThread::isPathCached(const SyncItem& next) const
714 {
715     TileAgeCache::const_iterator ii = _completedTiles.find( next._dir );
716     if (ii == _completedTiles.end()) {
717         ii = _notFoundItems.find( next._dir );
718         // Invalid means 'not cached', otherwise we want to return to
719         // higher levels the cache status
720         return (ii == _notFoundItems.end()) ? SyncItem::Invalid : SyncItem::NotFound;
721     }
722
723     // check if the path still physically exists. This is needed to
724     // cope with the user manipulating our cache dir
725     SGPath p(_local_dir);
726     p.append(next._dir);
727     if (!p.exists()) {
728         return SyncItem::Invalid;
729     }
730
731     time_t now = time(0);
732     return (ii->second > now) ? SyncItem::Cached : SyncItem::Invalid;
733 }
734
735 void SGTerraSync::SvnThread::fail(SyncItem failedItem)
736 {
737     time_t now = time(0);
738     _consecutive_errors++;
739     _fail_count++;
740     failedItem._status = SyncItem::Failed;
741     _freshTiles.push_back(failedItem);
742     SG_LOG(SG_TERRAIN,SG_INFO,
743            "Faield to sync'" << failedItem._dir << "'");
744     _completedTiles[ failedItem._dir ] = now + UpdateInterval::FailedAttempt;
745     _is_dirty = true;
746 }
747
748 void SGTerraSync::SvnThread::notFound(SyncItem item)
749 {
750     // treat not found as authorative, so use the same cache expiry
751     // as succesful download. Important for MP models and similar so
752     // we don't spam the server with lookups for models that don't
753     // exist
754
755     time_t now = time(0);
756     item._status = SyncItem::NotFound;
757     _freshTiles.push_back(item);
758     _is_dirty = true;
759     _notFoundItems[ item._dir ] = now + UpdateInterval::SuccessfulAttempt;
760     writeCompletedTilesPersistentCache();
761 }
762
763 void SGTerraSync::SvnThread::updated(SyncItem item, bool isNewDirectory)
764 {
765     time_t now = time(0);
766     _consecutive_errors = 0;
767     _success_count++;
768     SG_LOG(SG_TERRAIN,SG_INFO,
769            "Successfully synchronized directory '" << item._dir << "'");
770
771     item._status = SyncItem::Updated;
772     if (item._type == SyncItem::Tile) {
773         _updated_tile_count++;
774     }
775
776     _freshTiles.push_back(item);
777     _completedTiles[ item._dir ] = now + UpdateInterval::SuccessfulAttempt;
778     _is_dirty = true;
779     writeCompletedTilesPersistentCache();
780 }
781
782 void SGTerraSync::SvnThread::initCompletedTilesPersistentCache()
783 {
784     if (!_persistentCachePath.exists()) {
785         return;
786     }
787
788     SGPropertyNode_ptr cacheRoot(new SGPropertyNode);
789     time_t now = time(0);
790
791     try {
792         readProperties(_persistentCachePath.str(), cacheRoot);
793     } catch (sg_exception& e) {
794         SG_LOG(SG_TERRAIN, SG_INFO, "corrupted persistent cache, discarding");
795         return;
796     }
797
798     for (int i=0; i<cacheRoot->nChildren(); ++i) {
799         SGPropertyNode* entry = cacheRoot->getChild(i);
800         bool isNotFound = (strcmp(entry->getName(), "not-found") == 0);
801         string tileName = entry->getStringValue("path");
802         time_t stamp = entry->getIntValue("stamp");
803         if (stamp < now) {
804             continue;
805         }
806
807         if (isNotFound) {
808             _completedTiles[tileName] = stamp;
809         } else {
810             _notFoundItems[tileName] = stamp;
811         }
812     }
813 }
814
815 void SGTerraSync::SvnThread::writeCompletedTilesPersistentCache() const
816 {
817     // cache is disabled
818     if (_persistentCachePath.isNull()) {
819         return;
820     }
821
822     std::ofstream f(_persistentCachePath.c_str(), std::ios::trunc);
823     if (!f.is_open()) {
824         return;
825     }
826
827     SGPropertyNode_ptr cacheRoot(new SGPropertyNode);
828     TileAgeCache::const_iterator it = _completedTiles.begin();
829     for (; it != _completedTiles.end(); ++it) {
830         SGPropertyNode* entry = cacheRoot->addChild("entry");
831         entry->setStringValue("path", it->first);
832         entry->setIntValue("stamp", it->second);
833     }
834
835     it = _notFoundItems.begin();
836     for (; it != _notFoundItems.end(); ++it) {
837         SGPropertyNode* entry = cacheRoot->addChild("not-found");
838         entry->setStringValue("path", it->first);
839         entry->setIntValue("stamp", it->second);
840     }
841
842     writeProperties(f, cacheRoot, true /* write_all */);
843     f.close();
844 }
845
846 ///////////////////////////////////////////////////////////////////////////////
847 // SGTerraSync ////////////////////////////////////////////////////////////////
848 ///////////////////////////////////////////////////////////////////////////////
849 SGTerraSync::SGTerraSync() :
850     _svnThread(NULL),
851     _bound(false),
852     _inited(false)
853 {
854     _svnThread = new SvnThread();
855     _log = new BufferedLogCallback(SG_TERRAIN, SG_INFO);
856     _log->truncateAt(255);
857
858     sglog().addCallback(_log);
859 }
860
861 SGTerraSync::~SGTerraSync()
862 {
863     delete _svnThread;
864     _svnThread = NULL;
865     sglog().removeCallback(_log);
866     delete _log;
867      _tiedProperties.Untie();
868 }
869
870 void SGTerraSync::setRoot(SGPropertyNode_ptr root)
871 {
872     _terraRoot = root->getNode("/sim/terrasync",true);
873 }
874
875 void SGTerraSync::init()
876 {
877     if (_inited) {
878         return;
879     }
880
881     _inited = true;
882
883     assert(_terraRoot);
884     _terraRoot->setBoolValue("built-in-svn-available",svn_built_in_available);
885
886     reinit();
887 }
888
889 void SGTerraSync::shutdown()
890 {
891      _svnThread->stop();
892 }
893
894 void SGTerraSync::reinit()
895 {
896     // do not reinit when enabled and we're already up and running
897     if ((_terraRoot->getBoolValue("enabled",false))&&
898          (_svnThread->_active && _svnThread->_running))
899     {
900         return;
901     }
902
903     _svnThread->stop();
904
905     if (_terraRoot->getBoolValue("enabled",false))
906     {
907         _svnThread->setSvnServer(_terraRoot->getStringValue("svn-server",""));
908         _svnThread->setSvnDataServer(_terraRoot->getStringValue("svn-data-server",""));
909         _svnThread->setRsyncServer(_terraRoot->getStringValue("rsync-server",""));
910         _svnThread->setLocalDir(_terraRoot->getStringValue("scenery-dir",""));
911         _svnThread->setAllowedErrorCount(_terraRoot->getIntValue("max-errors",5));
912         _svnThread->setUseBuiltin(_terraRoot->getBoolValue("use-built-in-svn",true));
913         _svnThread->setCachePath(SGPath(_terraRoot->getStringValue("cache-path","")));
914         _svnThread->setCacheHits(_terraRoot->getIntValue("cache-hit", 0));
915         _svnThread->setUseSvn(_terraRoot->getBoolValue("use-svn",true));
916         _svnThread->setExtSvnUtility(_terraRoot->getStringValue("ext-svn-utility","svn"));
917
918         if (_svnThread->start())
919         {
920             syncAirportsModels();
921         }
922     }
923
924     _stalledNode->setBoolValue(_svnThread->_stalled);
925 }
926
927 void SGTerraSync::bind()
928 {
929     if (_bound) {
930         return;
931     }
932
933     _bound = true;
934     _tiedProperties.Tie( _terraRoot->getNode("busy", true), (bool*) &_svnThread->_busy );
935     _tiedProperties.Tie( _terraRoot->getNode("active", true), (bool*) &_svnThread->_active );
936     _tiedProperties.Tie( _terraRoot->getNode("update-count", true), (int*) &_svnThread->_success_count );
937     _tiedProperties.Tie( _terraRoot->getNode("error-count", true), (int*) &_svnThread->_fail_count );
938     _tiedProperties.Tie( _terraRoot->getNode("tile-count", true), (int*) &_svnThread->_updated_tile_count );
939     _tiedProperties.Tie( _terraRoot->getNode("cache-hits", true), (int*) &_svnThread->_cache_hits );
940     _tiedProperties.Tie( _terraRoot->getNode("transfer-rate-bytes-sec", true), (int*) &_svnThread->_transfer_rate );
941
942     // use kbytes here because propety doesn't support 64-bit and we might conceivably
943     // download more than 2G in a single session
944     _tiedProperties.Tie( _terraRoot->getNode("downloaded-kbytes", true), (int*) &_svnThread->_total_kb_downloaded );
945
946     _terraRoot->getNode("busy", true)->setAttribute(SGPropertyNode::WRITE,false);
947     _terraRoot->getNode("active", true)->setAttribute(SGPropertyNode::WRITE,false);
948     _terraRoot->getNode("update-count", true)->setAttribute(SGPropertyNode::WRITE,false);
949     _terraRoot->getNode("error-count", true)->setAttribute(SGPropertyNode::WRITE,false);
950     _terraRoot->getNode("tile-count", true)->setAttribute(SGPropertyNode::WRITE,false);
951     _terraRoot->getNode("use-built-in-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
952     _terraRoot->getNode("use-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
953     // stalled is used as a signal handler (to connect listeners triggering GUI pop-ups)
954     _stalledNode = _terraRoot->getNode("stalled", true);
955     _stalledNode->setBoolValue(_svnThread->_stalled);
956     _stalledNode->setAttribute(SGPropertyNode::PRESERVE,true);
957 }
958
959 void SGTerraSync::unbind()
960 {
961     _svnThread->stop();
962     _tiedProperties.Untie();
963     _bound = false;
964     _inited = false;
965
966     _terraRoot.clear();
967     _stalledNode.clear();
968     _cacheHits.clear();
969 }
970
971 void SGTerraSync::update(double)
972 {
973     static SGBucket bucket;
974     if (_svnThread->isDirty())
975     {
976         if (!_svnThread->_active)
977         {
978             if (_svnThread->_stalled)
979             {
980                 SG_LOG(SG_TERRAIN,SG_ALERT,
981                        "Automatic scenery download/synchronization stalled. Too many errors.");
982             }
983             else
984             {
985                 // not really an alert - just always show this message
986                 SG_LOG(SG_TERRAIN,SG_ALERT,
987                         "Automatic scenery download/synchronization has stopped.");
988             }
989             _stalledNode->setBoolValue(_svnThread->_stalled);
990         }
991
992         while (_svnThread->hasNewTiles())
993         {
994             SyncItem next = _svnThread->getNewTile();
995
996             if ((next._type == SyncItem::Tile) || (next._type == SyncItem::AIData)) {
997                 _activeTileDirs.erase(next._dir);
998             }
999         } // of freshly synced items
1000     }
1001 }
1002
1003 bool SGTerraSync::isIdle() {return _svnThread->isIdle();}
1004
1005 void SGTerraSync::syncAirportsModels()
1006 {
1007     static const char* bounds = "MZAJKL"; // airport sync order: K-L, A-J, M-Z
1008     // note "request" method uses LIFO order, i.e. processes most recent request first
1009     for( unsigned i = 0; i < strlen(bounds)/2; i++ )
1010     {
1011         for ( char synced_other = bounds[2*i]; synced_other <= bounds[2*i+1]; synced_other++ )
1012         {
1013             ostringstream dir;
1014             dir << "Airports/" << synced_other;
1015             SyncItem w(dir.str(), SyncItem::AirportData);
1016             _svnThread->request( w );
1017         }
1018     }
1019
1020     SyncItem w("Models", SyncItem::SharedModels);
1021     _svnThread->request( w );
1022 }
1023
1024 void SGTerraSync::syncAreaByPath(const std::string& aPath)
1025 {
1026     const char* terrainobjects[3] = { "Terrain/", "Objects/",  0 };
1027     for (const char** tree = &terrainobjects[0]; *tree; tree++)
1028     {
1029         std::string dir = string(*tree) + aPath;
1030         if (_activeTileDirs.find(dir) != _activeTileDirs.end()) {
1031             continue;
1032         }
1033
1034         _activeTileDirs.insert(dir);
1035         SyncItem w(dir, SyncItem::Tile);
1036         _svnThread->request( w );
1037     }
1038 }
1039
1040 bool SGTerraSync::scheduleTile(const SGBucket& bucket)
1041 {
1042     std::string basePath = bucket.gen_base_path();
1043     syncAreaByPath(basePath);
1044     return true;
1045 }
1046
1047 bool SGTerraSync::isTileDirPending(const std::string& sceneryDir) const
1048 {
1049     if (!_svnThread->_running) {
1050         return false;
1051     }
1052
1053     const char* terrainobjects[3] = { "Terrain/", "Objects/",  0 };
1054     for (const char** tree = &terrainobjects[0]; *tree; tree++) {
1055         string s = *tree + sceneryDir;
1056         if (_activeTileDirs.find(s) != _activeTileDirs.end()) {
1057             return true;
1058         }
1059     }
1060
1061     return false;
1062 }
1063
1064 void SGTerraSync::scheduleDataDir(const std::string& dataDir)
1065 {
1066     if (_activeTileDirs.find(dataDir) != _activeTileDirs.end()) {
1067         return;
1068     }
1069
1070     _activeTileDirs.insert(dataDir);
1071     SyncItem w(dataDir, SyncItem::AIData);
1072     _svnThread->request( w );
1073
1074 }
1075
1076 bool SGTerraSync::isDataDirPending(const std::string& dataDir) const
1077 {
1078     if (!_svnThread->_running) {
1079         return false;
1080     }
1081
1082     return (_activeTileDirs.find(dataDir) != _activeTileDirs.end());
1083 }
1084
1085 void SGTerraSync::reposition()
1086 {
1087     // stub, remove
1088 }