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