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