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