]> git.mxchange.org Git - simgear.git/blob - simgear/scene/tsync/terrasync.cxx
Terrasync logging tweaks
[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 ///////////////////////////////////////////////////////////////////////////////
204 // SGTerraSync::SvnThread /////////////////////////////////////////////////////
205 ///////////////////////////////////////////////////////////////////////////////
206 class SGTerraSync::SvnThread : public SGThread
207 {
208 public:
209    SvnThread();
210    virtual ~SvnThread( ) { stop(); }
211
212    void stop();
213    bool start();
214
215     bool isIdle() {return !_busy; }
216    void request(const SyncItem& dir) {waitingTiles.push_front(dir);}
217    bool isDirty() { bool r = _is_dirty;_is_dirty = false;return r;}
218    bool hasNewTiles() { return !_freshTiles.empty();}
219    SyncItem getNewTile() { return _freshTiles.pop_front();}
220
221    void   setSvnServer(string server)       { _svn_server   = stripPath(server);}
222    void   setSvnDataServer(string server)   { _svn_data_server   = stripPath(server);}
223     
224    void   setExtSvnUtility(string svn_util) { _svn_command  = simgear::strutils::strip(svn_util);}
225    void   setRsyncServer(string server)     { _rsync_server = simgear::strutils::strip(server);}
226    void   setLocalDir(string dir)           { _local_dir    = stripPath(dir);}
227    string getLocalDir()                     { return _local_dir;}
228    void   setUseSvn(bool use_svn)           { _use_svn = use_svn;}
229    void   setAllowedErrorCount(int errors)  {_allowed_errors = errors;}
230
231    void   setCachePath(const SGPath& p)     {_persistentCachePath = p;}
232    void   setCacheHits(unsigned int hits)   {_cache_hits = hits;}
233    void   setUseBuiltin(bool built_in) { _use_built_in = built_in;}
234
235    volatile bool _active;
236    volatile bool _running;
237    volatile bool _busy;
238    volatile bool _stalled;
239    volatile int  _fail_count;
240    volatile int  _updated_tile_count;
241    volatile int  _success_count;
242    volatile int  _consecutive_errors;
243    volatile int  _allowed_errors;
244    volatile int  _cache_hits;
245    volatile int _transfer_rate;
246    // kbytes, not bytes, because bytes might overflow 2^31
247    volatile int _total_kb_downloaded;
248    
249 private:
250    virtual void run();
251     
252     // external model run and helpers
253     void runExternal();
254     void syncPathExternal(const SyncItem& next);
255     bool runExternalSyncCommand(const char* dir);
256
257     // internal mode run and helpers
258     void runInternal();
259     void updateSyncSlot(SyncSlot& slot);
260
261     // commond helpers between both internal and external models
262     
263     SyncItem::Status isPathCached(const SyncItem& next) const;
264     void initCompletedTilesPersistentCache();
265     void writeCompletedTilesPersistentCache() const;
266     void updated(SyncItem item, bool isNewDirectory);
267     void fail(SyncItem failedItem);
268     void notFound(SyncItem notFoundItem);
269     
270     bool _use_built_in;
271     HTTP::Client _http;
272     SyncSlot _syncSlots[NUM_SYNC_SLOTS];
273
274    volatile bool _is_dirty;
275    volatile bool _stop;
276    SGBlockingDeque <SyncItem> waitingTiles;
277    
278    TileAgeCache _completedTiles;
279    TileAgeCache _notFoundItems;
280    
281    SGBlockingDeque <SyncItem> _freshTiles;
282    bool _use_svn;
283    string _svn_server;
284    string _svn_data_server;
285    string _svn_command;
286    string _rsync_server;
287    string _local_dir;
288    SGPath _persistentCachePath;
289 };
290
291 SGTerraSync::SvnThread::SvnThread() :
292     _active(false),
293     _running(false),
294     _busy(false),
295     _stalled(false),
296     _fail_count(0),
297     _updated_tile_count(0),
298     _success_count(0),
299     _consecutive_errors(0),
300     _allowed_errors(6),
301     _cache_hits(0),
302     _transfer_rate(0),
303     _total_kb_downloaded(0),
304     _use_built_in(true),
305     _is_dirty(false),
306     _stop(false),
307     _use_svn(true)
308 {
309     _http.setUserAgent("terrascenery-" SG_STRINGIZE(SG_VERSION));
310 }
311
312 void SGTerraSync::SvnThread::stop()
313 {
314     // drop any pending requests
315     waitingTiles.clear();
316
317     if (!_running)
318         return;
319
320     // set stop flag and wake up the thread with an empty request
321     _stop = true;
322     SyncItem w(string(), SyncItem::Stop);
323     request(w);
324     join();
325     _running = false;
326 }
327
328 bool SGTerraSync::SvnThread::start()
329 {
330     if (_running)
331         return false;
332
333     if (_local_dir=="")
334     {
335         SG_LOG(SG_TERRAIN,SG_ALERT,
336                "Cannot start scenery download. Local cache directory is undefined.");
337         _fail_count++;
338         _stalled = true;
339         return false;
340     }
341
342     SGPath path(_local_dir);
343     if (!path.exists())
344     {
345         SG_LOG(SG_TERRAIN,SG_ALERT,
346                "Cannot start scenery download. Directory '" << _local_dir <<
347                "' does not exist. Set correct directory path or create directory folder.");
348         _fail_count++;
349         _stalled = true;
350         return false;
351     }
352
353     path.append("version");
354     if (path.exists())
355     {
356         SG_LOG(SG_TERRAIN,SG_ALERT,
357                "Cannot start scenery download. Directory '" << _local_dir <<
358                "' contains the base package. Use a separate directory.");
359         _fail_count++;
360         _stalled = true;
361         return false;
362     }
363
364     _use_svn |= _use_built_in;
365
366     if ((_use_svn)&&(_svn_server==""))
367     {
368         SG_LOG(SG_TERRAIN,SG_ALERT,
369                "Cannot start scenery download. Subversion scenery server is undefined.");
370         _fail_count++;
371         _stalled = true;
372         return false;
373     }
374     if ((!_use_svn)&&(_rsync_server==""))
375     {
376         SG_LOG(SG_TERRAIN,SG_ALERT,
377                "Cannot start scenery download. Rsync scenery server is undefined.");
378         _fail_count++;
379         _stalled = true;
380         return false;
381     }
382
383     _fail_count = 0;
384     _updated_tile_count = 0;
385     _success_count = 0;
386     _consecutive_errors = 0;
387     _stop = false;
388     _stalled = false;
389     _running = true;
390
391     string status;
392
393     if (_use_svn && _use_built_in)
394         status = "Using built-in SVN support. ";
395     else if (_use_svn)
396     {
397         status = "Using external SVN utility '";
398         status += _svn_command;
399         status += "'. ";
400     }
401     else
402     {
403         status = "Using RSYNC. ";
404     }
405
406     // not really an alert - but we want to (always) see this message, so user is
407     // aware we're downloading scenery (and using bandwidth).
408     SG_LOG(SG_TERRAIN,SG_ALERT,
409            "Starting automatic scenery download/synchronization. "
410            << status
411            << "Directory: '" << _local_dir << "'.");
412
413     SGThread::start();
414     return true;
415 }
416
417 bool SGTerraSync::SvnThread::runExternalSyncCommand(const char* dir)
418 {
419     ostringstream buf;
420     SGPath localPath( _local_dir );
421     localPath.append( dir );
422
423     if (_use_svn)
424     {
425         buf << "\"" << _svn_command << "\" "
426             << svn_options << " "
427             << "\"" << _svn_server << "/" << dir << "\" "
428             << "\"" << localPath.str_native() << "\"";
429     } else {
430         buf << rsync_cmd << " "
431             << "\"" << _rsync_server << "/" << dir << "/\" "
432             << "\"" << localPath.str_native() << "/\"";
433     }
434
435     string command;
436 #ifdef SG_WINDOWS
437         // windows command line parsing is just lovely...
438         // to allow white spaces, the system call needs this:
439         // ""C:\Program Files\something.exe" somearg "some other arg""
440         // Note: whitespace strings quoted by a pair of "" _and_ the 
441         //       entire string needs to be wrapped by "" too.
442         // The svn url needs forward slashes (/) as a path separator while
443         // the local path needs windows-native backslash as a path separator.
444     command = "\"" + buf.str() + "\"";
445 #else
446     command = buf.str();
447 #endif
448     SG_LOG(SG_TERRAIN,SG_DEBUG, "sync command '" << command << "'");
449
450 #ifdef SG_WINDOWS
451     // tbd: does Windows support "popen"?
452     int rc = system( command.c_str() );
453 #else
454     FILE* pipe = popen( command.c_str(), "r");
455     int rc=-1;
456     // wait for external process to finish
457     if (pipe)
458         rc = pclose(pipe);
459 #endif
460
461     if (rc)
462     {
463         SG_LOG(SG_TERRAIN,SG_ALERT,
464                "Failed to synchronize directory '" << dir << "', " <<
465                "error code= " << rc);
466         return false;
467     }
468     return true;
469 }
470
471 void SGTerraSync::SvnThread::run()
472 {
473     _active = true;
474     initCompletedTilesPersistentCache();
475     
476     if (_use_built_in) {
477         runInternal();
478     } else {
479         runExternal();
480     }
481     
482     _active = false;
483     _running = false;
484     _is_dirty = true;
485 }
486
487 void SGTerraSync::SvnThread::runExternal()
488 {
489     while (!_stop)
490     {
491         SyncItem next = waitingTiles.pop_front();
492         if (_stop)
493            break;
494
495         SyncItem::Status cacheStatus = isPathCached(next);
496         if (cacheStatus != SyncItem::Invalid) {
497             _cache_hits++;
498             SG_LOG(SG_TERRAIN, SG_DEBUG,
499                    "Cache hit for: '" << next._dir << "'");
500             next._status = cacheStatus;
501             _freshTiles.push_back(next);
502             _is_dirty = true;
503             continue;
504         }
505         
506         syncPathExternal(next);
507
508         if ((_allowed_errors >= 0)&&
509             (_consecutive_errors >= _allowed_errors))
510         {
511             _stalled = true;
512             _stop = true;
513         }
514     } // of thread running loop
515 }
516
517 void SGTerraSync::SvnThread::syncPathExternal(const SyncItem& next)
518 {
519     _busy = true;
520     SGPath path( _local_dir );
521     path.append( next._dir );
522     bool isNewDirectory = !path.exists();
523     
524     try {
525         if (isNewDirectory) {
526             int rc = path.create_dir( 0755 );
527             if (rc) {
528                 SG_LOG(SG_TERRAIN,SG_ALERT,
529                        "Cannot create directory '" << path << "', return code = " << rc );
530                 throw sg_exception("Cannot create directory for terrasync", path.str());
531             }
532         }
533         
534         if (!runExternalSyncCommand(next._dir.c_str())) {
535             throw sg_exception("Running external sync command failed");
536         }
537     } catch (sg_exception& e) {
538         fail(next);
539         _busy = false;
540         return;
541     }
542     
543     updated(next, isNewDirectory);
544     _busy = false;
545 }
546
547 void SGTerraSync::SvnThread::updateSyncSlot(SyncSlot &slot)
548 {
549     if (slot.repository.get()) {
550         if (slot.repository->isDoingSync()) {
551 #if 1
552             if (slot.stamp.elapsedMSec() > slot.nextWarnTimeout) {
553                 SG_LOG(SG_TERRAIN, SG_INFO, "sync taking a long time:" << slot.currentItem._dir << " taken " << slot.stamp.elapsedMSec());
554                 SG_LOG(SG_TERRAIN, SG_INFO, "HTTP status:" << _http.hasActiveRequests());
555                 slot.nextWarnTimeout += 10000;
556             }
557 #endif
558             return; // easy, still working
559         }
560         
561         // check result
562         SVNRepository::ResultCode res = slot.repository->failure();
563         if (res == SVNRepository::SVN_ERROR_NOT_FOUND) {
564             notFound(slot.currentItem);
565         } else if (res != SVNRepository::SVN_NO_ERROR) {
566             fail(slot.currentItem);
567         } else {
568             updated(slot.currentItem, slot.isNewDirectory);
569             SG_LOG(SG_TERRAIN, SG_DEBUG, "sync of " << slot.repository->baseUrl() << " finished ("
570                    << slot.stamp.elapsedMSec() << " msec");
571         }
572
573         // whatever happened, we're done with this repository instance
574         slot.busy = false;
575         slot.repository.reset();
576     }
577     
578     // init and start sync of the next repository
579     if (!slot.queue.empty()) {
580         slot.currentItem = slot.queue.front();
581         slot.queue.pop();
582         
583         SGPath path(_local_dir);
584         path.append(slot.currentItem._dir);
585         slot.isNewDirectory = !path.exists();
586         if (slot.isNewDirectory) {
587             int rc = path.create_dir( 0755 );
588             if (rc) {
589                 SG_LOG(SG_TERRAIN,SG_ALERT,
590                        "Cannot create directory '" << path << "', return code = " << rc );
591                 fail(slot.currentItem);
592                 return;
593             }
594         } // of creating directory step
595         
596         string serverUrl(_svn_server);
597         if (slot.currentItem._type == SyncItem::AIData) {
598             serverUrl = _svn_data_server;
599         }
600         
601         slot.repository.reset(new SVNRepository(path, &_http));
602         slot.repository->setBaseUrl(serverUrl + "/" + slot.currentItem._dir);
603         slot.repository->update();
604         
605         slot.nextWarnTimeout = 20000;
606         slot.stamp.stamp();
607         slot.busy = true;
608         SG_LOG(SG_TERRAIN, SG_INFO, "sync of " << slot.repository->baseUrl() << " started, queue size is " << slot.queue.size());
609     }
610 }
611
612 void SGTerraSync::SvnThread::runInternal()
613 {
614     while (!_stop) {
615         _http.update(100);
616         _transfer_rate = _http.transferRateBytesPerSec();
617         // convert from bytes to kbytes
618         _total_kb_downloaded = static_cast<int>(_http.totalBytesDownloaded() / 1024);
619         
620         if (_stop)
621             break;
622  
623         // drain the waiting tiles queue into the sync slot queues.
624         while (!waitingTiles.empty()) {
625             SyncItem next = waitingTiles.pop_front();
626             SyncItem::Status cacheStatus = isPathCached(next);
627             if (cacheStatus != SyncItem::Invalid) {
628                 _cache_hits++;
629                 SG_LOG(SG_TERRAIN, SG_DEBUG, "\nTerraSync Cache hit for: '" << next._dir << "'");
630                 next._status = cacheStatus;
631                 _freshTiles.push_back(next);
632                 _is_dirty = true;
633                 continue;
634             }
635
636             unsigned int slot = syncSlotForType(next._type);
637             _syncSlots[slot].queue.push(next);
638         }
639        
640         bool anySlotBusy = false;
641         // update each sync slot in turn
642         for (unsigned int slot=0; slot < NUM_SYNC_SLOTS; ++slot) {
643             updateSyncSlot(_syncSlots[slot]);
644             anySlotBusy |= _syncSlots[slot].busy;
645         }
646
647         _busy = anySlotBusy;
648         if (!anySlotBusy) {
649             // wait on the blocking deque here, otherwise we spin
650             // the loop very fast, since _http::update with no connections
651             // active returns immediately.
652             waitingTiles.waitOnNotEmpty();
653         }
654     } // of thread running loop
655 }
656
657 SyncItem::Status SGTerraSync::SvnThread::isPathCached(const SyncItem& next) const
658 {
659     TileAgeCache::const_iterator ii = _completedTiles.find( next._dir );
660     if (ii == _completedTiles.end()) {
661         ii = _notFoundItems.find( next._dir );
662         // Invalid means 'not cached', otherwise we want to return to
663         // higher levels the cache status
664         return (ii == _notFoundItems.end()) ? SyncItem::Invalid : SyncItem::NotFound;
665     }
666     
667     // check if the path still physically exists. This is needed to
668     // cope with the user manipulating our cache dir
669     SGPath p(_local_dir);
670     p.append(next._dir);
671     if (!p.exists()) {
672         return SyncItem::Invalid;
673     }
674     
675     time_t now = time(0);
676     return (ii->second > now) ? SyncItem::Cached : SyncItem::Invalid;
677 }
678
679 void SGTerraSync::SvnThread::fail(SyncItem failedItem)
680 {
681     time_t now = time(0);
682     _consecutive_errors++;
683     _fail_count++;
684     failedItem._status = SyncItem::Failed;
685     _freshTiles.push_back(failedItem);
686     SG_LOG(SG_TERRAIN,SG_INFO,
687            "Faield to sync'" << failedItem._dir << "'");
688     _completedTiles[ failedItem._dir ] = now + UpdateInterval::FailedAttempt;
689     _is_dirty = true;
690 }
691
692 void SGTerraSync::SvnThread::notFound(SyncItem item)
693 {
694     // treat not found as authorative, so use the same cache expiry
695     // as succesful download. Important for MP models and similar so
696     // we don't spam the server with lookups for models that don't
697     // exist
698     
699     time_t now = time(0);
700     item._status = SyncItem::NotFound;
701     _freshTiles.push_back(item);
702     _is_dirty = true;
703     _notFoundItems[ item._dir ] = now + UpdateInterval::SuccessfulAttempt;
704     writeCompletedTilesPersistentCache();
705 }
706
707 void SGTerraSync::SvnThread::updated(SyncItem item, bool isNewDirectory)
708 {
709     time_t now = time(0);
710     _consecutive_errors = 0;
711     _success_count++;
712     SG_LOG(SG_TERRAIN,SG_INFO,
713            "Successfully synchronized directory '" << item._dir << "'");
714     
715     item._status = SyncItem::Updated;
716     if (item._type == SyncItem::Tile) {
717         _updated_tile_count++;
718     }
719
720     _freshTiles.push_back(item);
721     _completedTiles[ item._dir ] = now + UpdateInterval::SuccessfulAttempt;
722     _is_dirty = true;
723     writeCompletedTilesPersistentCache();
724 }
725
726 void SGTerraSync::SvnThread::initCompletedTilesPersistentCache()
727 {
728     if (!_persistentCachePath.exists()) {
729         return;
730     }
731     
732     SGPropertyNode_ptr cacheRoot(new SGPropertyNode);
733     time_t now = time(0);
734     
735     try {
736         readProperties(_persistentCachePath.str(), cacheRoot);
737     } catch (sg_exception& e) {
738         SG_LOG(SG_TERRAIN, SG_INFO, "corrupted persistent cache, discarding");
739         return;
740     }
741     
742     for (int i=0; i<cacheRoot->nChildren(); ++i) {
743         SGPropertyNode* entry = cacheRoot->getChild(i);
744         bool isNotFound = (strcmp(entry->getName(), "not-found") == 0);
745         string tileName = entry->getStringValue("path");
746         time_t stamp = entry->getIntValue("stamp");
747         if (stamp < now) {
748             continue;
749         }
750         
751         if (isNotFound) {
752             _completedTiles[tileName] = stamp;
753         } else {
754             _notFoundItems[tileName] = stamp;
755         }
756     }
757 }
758
759 void SGTerraSync::SvnThread::writeCompletedTilesPersistentCache() const
760 {
761     // cache is disabled
762     if (_persistentCachePath.isNull()) {
763         return;
764     }
765     
766     std::ofstream f(_persistentCachePath.c_str(), std::ios::trunc);
767     if (!f.is_open()) {
768         return;
769     }
770     
771     SGPropertyNode_ptr cacheRoot(new SGPropertyNode);
772     TileAgeCache::const_iterator it = _completedTiles.begin();
773     for (; it != _completedTiles.end(); ++it) {
774         SGPropertyNode* entry = cacheRoot->addChild("entry");
775         entry->setStringValue("path", it->first);
776         entry->setIntValue("stamp", it->second);
777     }
778     
779     it = _notFoundItems.begin();
780     for (; it != _notFoundItems.end(); ++it) {
781         SGPropertyNode* entry = cacheRoot->addChild("not-found");
782         entry->setStringValue("path", it->first);
783         entry->setIntValue("stamp", it->second);
784     }
785     
786     writeProperties(f, cacheRoot, true /* write_all */);
787     f.close();
788 }
789
790 ///////////////////////////////////////////////////////////////////////////////
791 // SGTerraSync ////////////////////////////////////////////////////////////////
792 ///////////////////////////////////////////////////////////////////////////////
793 SGTerraSync::SGTerraSync() :
794     _svnThread(NULL),
795     _bound(false),
796     _inited(false)
797 {
798     _svnThread = new SvnThread();
799     _log = new BufferedLogCallback(SG_TERRAIN, SG_INFO);
800     _log->truncateAt(255);
801     
802     sglog().addCallback(_log);
803 }
804
805 SGTerraSync::~SGTerraSync()
806 {
807     delete _svnThread;
808     _svnThread = NULL;
809     sglog().removeCallback(_log);
810     delete _log;
811      _tiedProperties.Untie();
812 }
813
814 void SGTerraSync::setRoot(SGPropertyNode_ptr root)
815 {
816     _terraRoot = root->getNode("/sim/terrasync",true);
817 }
818
819 void SGTerraSync::init()
820 {
821     if (_inited) {
822         return;
823     }
824     
825     _inited = true;
826     
827     assert(_terraRoot);
828     _terraRoot->setBoolValue("built-in-svn-available",svn_built_in_available);
829         
830     reinit();
831 }
832
833 void SGTerraSync::shutdown()
834 {
835      _svnThread->stop();
836 }
837
838 void SGTerraSync::reinit()
839 {
840     // do not reinit when enabled and we're already up and running
841     if ((_terraRoot->getBoolValue("enabled",false))&&
842          (_svnThread->_active && _svnThread->_running))
843     {
844         return;
845     }
846     
847     _svnThread->stop();
848
849     if (_terraRoot->getBoolValue("enabled",false))
850     {
851         _svnThread->setSvnServer(_terraRoot->getStringValue("svn-server",""));
852         _svnThread->setSvnDataServer(_terraRoot->getStringValue("svn-data-server",""));
853         _svnThread->setRsyncServer(_terraRoot->getStringValue("rsync-server",""));
854         _svnThread->setLocalDir(_terraRoot->getStringValue("scenery-dir",""));
855         _svnThread->setAllowedErrorCount(_terraRoot->getIntValue("max-errors",5));
856         _svnThread->setUseBuiltin(_terraRoot->getBoolValue("use-built-in-svn",true));
857         _svnThread->setCachePath(SGPath(_terraRoot->getStringValue("cache-path","")));
858         _svnThread->setCacheHits(_terraRoot->getIntValue("cache-hit", 0));
859         _svnThread->setUseSvn(_terraRoot->getBoolValue("use-svn",true));
860         _svnThread->setExtSvnUtility(_terraRoot->getStringValue("ext-svn-utility","svn"));
861
862         if (_svnThread->start())
863         {
864             syncAirportsModels();
865         }
866     }
867
868     _stalledNode->setBoolValue(_svnThread->_stalled);
869 }
870
871 void SGTerraSync::bind()
872 {
873     if (_bound) {
874         return;
875     }
876     
877     _bound = true;
878     _tiedProperties.Tie( _terraRoot->getNode("busy", true), (bool*) &_svnThread->_busy );
879     _tiedProperties.Tie( _terraRoot->getNode("active", true), (bool*) &_svnThread->_active );
880     _tiedProperties.Tie( _terraRoot->getNode("update-count", true), (int*) &_svnThread->_success_count );
881     _tiedProperties.Tie( _terraRoot->getNode("error-count", true), (int*) &_svnThread->_fail_count );
882     _tiedProperties.Tie( _terraRoot->getNode("tile-count", true), (int*) &_svnThread->_updated_tile_count );
883     _tiedProperties.Tie( _terraRoot->getNode("cache-hits", true), (int*) &_svnThread->_cache_hits );
884     _tiedProperties.Tie( _terraRoot->getNode("transfer-rate-bytes-sec", true), (int*) &_svnThread->_transfer_rate );
885     
886     // use kbytes here because propety doesn't support 64-bit and we might conceivably
887     // download more than 2G in a single session
888     _tiedProperties.Tie( _terraRoot->getNode("downloaded-kbytes", true), (int*) &_svnThread->_total_kb_downloaded );
889     
890     _terraRoot->getNode("busy", true)->setAttribute(SGPropertyNode::WRITE,false);
891     _terraRoot->getNode("active", true)->setAttribute(SGPropertyNode::WRITE,false);
892     _terraRoot->getNode("update-count", true)->setAttribute(SGPropertyNode::WRITE,false);
893     _terraRoot->getNode("error-count", true)->setAttribute(SGPropertyNode::WRITE,false);
894     _terraRoot->getNode("tile-count", true)->setAttribute(SGPropertyNode::WRITE,false);
895     _terraRoot->getNode("use-built-in-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
896     _terraRoot->getNode("use-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
897     // stalled is used as a signal handler (to connect listeners triggering GUI pop-ups)
898     _stalledNode = _terraRoot->getNode("stalled", true);
899     _stalledNode->setBoolValue(_svnThread->_stalled);
900     _stalledNode->setAttribute(SGPropertyNode::PRESERVE,true);
901 }
902
903 void SGTerraSync::unbind()
904 {
905     _svnThread->stop();
906     _tiedProperties.Untie();
907     _bound = false;
908     _inited = false;
909     
910     _terraRoot.clear();
911     _stalledNode.clear();
912     _cacheHits.clear();
913 }
914
915 void SGTerraSync::update(double)
916 {
917     static SGBucket bucket;
918     if (_svnThread->isDirty())
919     {
920         if (!_svnThread->_active)
921         {
922             if (_svnThread->_stalled)
923             {
924                 SG_LOG(SG_TERRAIN,SG_ALERT,
925                        "Automatic scenery download/synchronization stalled. Too many errors.");
926             }
927             else
928             {
929                 // not really an alert - just always show this message
930                 SG_LOG(SG_TERRAIN,SG_ALERT,
931                         "Automatic scenery download/synchronization has stopped.");
932             }
933             _stalledNode->setBoolValue(_svnThread->_stalled);
934         }
935
936         while (_svnThread->hasNewTiles())
937         {
938             SyncItem next = _svnThread->getNewTile();
939             
940             if ((next._type == SyncItem::Tile) || (next._type == SyncItem::AIData)) {
941                 _activeTileDirs.erase(next._dir);
942             }
943         } // of freshly synced items
944     }
945 }
946
947 bool SGTerraSync::isIdle() {return _svnThread->isIdle();}
948
949 void SGTerraSync::syncAirportsModels()
950 {
951     static const char* bounds = "MZAJKL"; // airport sync order: K-L, A-J, M-Z
952     // note "request" method uses LIFO order, i.e. processes most recent request first
953     for( unsigned i = 0; i < strlen(bounds)/2; i++ )
954     {
955         for ( char synced_other = bounds[2*i]; synced_other <= bounds[2*i+1]; synced_other++ )
956         {
957             ostringstream dir;
958             dir << "Airports/" << synced_other;
959             SyncItem w(dir.str(), SyncItem::AirportData);
960             _svnThread->request( w );
961         }
962     }
963     
964     SyncItem w("Models", SyncItem::SharedModels);
965     _svnThread->request( w );
966 }
967
968 void SGTerraSync::syncAreaByPath(const std::string& aPath)
969 {
970     const char* terrainobjects[3] = { "Terrain/", "Objects/",  0 };
971     for (const char** tree = &terrainobjects[0]; *tree; tree++)
972     {
973         std::string dir = string(*tree) + aPath;
974         if (_activeTileDirs.find(dir) != _activeTileDirs.end()) {
975             continue;
976         }
977                 
978         _activeTileDirs.insert(dir);
979         SyncItem w(dir, SyncItem::Tile);
980         _svnThread->request( w );
981     }
982 }
983
984 bool SGTerraSync::scheduleTile(const SGBucket& bucket)
985 {
986     std::string basePath = bucket.gen_base_path();
987     syncAreaByPath(basePath);
988     return true;
989 }
990
991 bool SGTerraSync::isTileDirPending(const std::string& sceneryDir) const
992 {
993     if (!_svnThread->_running) {
994         return false;
995     }
996     
997     const char* terrainobjects[3] = { "Terrain/", "Objects/",  0 };
998     for (const char** tree = &terrainobjects[0]; *tree; tree++) {
999         string s = *tree + sceneryDir;
1000         if (_activeTileDirs.find(s) != _activeTileDirs.end()) {
1001             return true;
1002         }
1003     }
1004
1005     return false;
1006 }
1007
1008 void SGTerraSync::scheduleDataDir(const std::string& dataDir)
1009 {
1010     if (_activeTileDirs.find(dataDir) != _activeTileDirs.end()) {
1011         return;
1012     }
1013     
1014     _activeTileDirs.insert(dataDir);
1015     SyncItem w(dataDir, SyncItem::AIData);
1016     _svnThread->request( w );
1017
1018 }
1019
1020 bool SGTerraSync::isDataDirPending(const std::string& dataDir) const
1021 {
1022     if (!_svnThread->_running) {
1023         return false;
1024     }
1025     
1026     return (_activeTileDirs.find(dataDir) != _activeTileDirs.end());
1027 }
1028
1029 void SGTerraSync::reposition()
1030 {
1031     // stub, remove
1032 }