]> git.mxchange.org Git - simgear.git/blob - simgear/scene/tsync/terrasync.cxx
Persistent SVN update cache.
[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 #   ifndef HAVE_SVN_CLIENT_H
41 #       include <time.h>
42 #       include <process.h>
43 #   endif
44 #endif
45
46 #include <stdlib.h>             // atoi() atof() abs() system()
47 #include <signal.h>             // signal()
48 #include <string.h>
49
50 #include <iostream>
51 #include <fstream>
52 #include <string>
53 #include <map>
54
55 #include <simgear/compiler.h>
56
57 #include "terrasync.hxx"
58
59 #include <simgear/bucket/newbucket.hxx>
60 #include <simgear/misc/sg_path.hxx>
61 #include <simgear/misc/strutils.hxx>
62 #include <simgear/threads/SGQueue.hxx>
63 #include <simgear/misc/sg_dir.hxx>
64 #include <simgear/debug/BufferedLogCallback.hxx>
65 #include <simgear/props/props_io.hxx>
66
67 #ifdef SG_SVN_CLIENT
68 #  include <simgear/io/HTTPClient.hxx>
69 #  include <simgear/io/SVNRepository.hxx>
70 #endif
71
72 #ifdef HAVE_SVN_CLIENT_H
73 #  ifdef HAVE_LIBSVN_CLIENT_1
74 #    include <svn_version.h>
75 #    include <svn_auth.h>
76 #    include <svn_client.h>
77 #    include <svn_cmdline.h>
78 #    include <svn_pools.h>
79 #  else
80 #    undef HAVE_SVN_CLIENT_H
81 #  endif
82 #endif
83
84 #ifdef HAVE_SVN_CLIENT_H
85     static const svn_version_checklist_t mysvn_checklist[] = {
86         { "svn_subr",   svn_subr_version },
87         { "svn_client", svn_client_version },
88         { NULL, NULL }
89     };
90 #endif
91     
92 #if defined(HAVE_SVN_CLIENT_H) || defined(SG_SVN_CLIENT)
93     static const bool svn_built_in_available = true;
94 #else
95     static const bool svn_built_in_available = false;
96 #endif
97
98 using namespace simgear;
99 using namespace std;
100
101 const char* rsync_cmd = 
102         "rsync --verbose --archive --delete --perms --owner --group";
103
104 const char* svn_options =
105         "checkout -q";
106
107 namespace UpdateInterval
108 {
109     // interval in seconds to allow an update to repeat after a successful update (=daily)
110     static const double SuccessfulAttempt = 24*60*60;
111     // interval in seconds to allow another update after a failed attempt (10 minutes)
112     static const double FailedAttempt     = 10*60;
113 }
114
115 typedef map<string,time_t> CompletedTiles;
116
117 ///////////////////////////////////////////////////////////////////////////////
118 // helper functions ///////////////////////////////////////////////////////////
119 ///////////////////////////////////////////////////////////////////////////////
120 string stripPath(string path)
121 {
122     // svn doesn't like trailing white-spaces or path separators - strip them!
123     path = simgear::strutils::strip(path);
124     size_t slen = path.length();
125     while ((slen>0)&&
126             ((path[slen-1]=='/')||(path[slen-1]=='\\')))
127     {
128         slen--;
129     }
130     return path.substr(0,slen);
131 }
132
133 bool hasWhitespace(string path)
134 {
135     return path.find(' ')!=string::npos;
136 }
137
138 ///////////////////////////////////////////////////////////////////////////////
139 // WaitingTile ////////////////////////////////////////////////////////////////
140 ///////////////////////////////////////////////////////////////////////////////
141 class  WaitingTile
142 {
143 public:
144     WaitingTile(string dir,bool refresh) :
145         _dir(dir), _refreshScenery(refresh) {}
146     string _dir;
147     bool _refreshScenery;
148 };
149
150 ///////////////////////////////////////////////////////////////////////////////
151 // SGTerraSync::SvnThread /////////////////////////////////////////////////////
152 ///////////////////////////////////////////////////////////////////////////////
153 class SGTerraSync::SvnThread : public SGThread
154 {
155 public:
156    SvnThread();
157    virtual ~SvnThread( ) { stop(); }
158
159    void stop();
160    bool start();
161
162    bool isIdle() {return waitingTiles.empty();}
163    void request(const WaitingTile& dir) {waitingTiles.push_front(dir);}
164    bool isDirty() { bool r = _is_dirty;_is_dirty = false;return r;}
165    bool hasNewTiles() { return !_freshTiles.empty();}
166    WaitingTile getNewTile() { return _freshTiles.pop_front();}
167
168    void   setSvnServer(string server)       { _svn_server   = stripPath(server);}
169    void   setExtSvnUtility(string svn_util) { _svn_command  = simgear::strutils::strip(svn_util);}
170    void   setRsyncServer(string server)     { _rsync_server = simgear::strutils::strip(server);}
171    void   setLocalDir(string dir)           { _local_dir    = stripPath(dir);}
172    string getLocalDir()                     { return _local_dir;}
173    void   setUseSvn(bool use_svn)           { _use_svn = use_svn;}
174    void   setAllowedErrorCount(int errors)  {_allowed_errors = errors;}
175
176 #if defined(HAVE_SVN_CLIENT_H) || defined(SG_SVN_CLIENT)
177
178    void   setCachePath(const SGPath& p)     {_persistentCachePath = p;}
179    void   setCacheHits(unsigned int hits)   {_cache_hits = hits;}
180    void   setUseBuiltin(bool built_in) { _use_built_in = built_in;}
181 #endif
182
183    volatile bool _active;
184    volatile bool _running;
185    volatile bool _busy;
186    volatile bool _stalled;
187    volatile int  _fail_count;
188    volatile int  _updated_tile_count;
189    volatile int  _success_count;
190    volatile int  _consecutive_errors;
191    volatile int  _allowed_errors;
192    volatile int  _cache_hits;
193 private:
194    virtual void run();
195    bool syncTree(const char* dir, bool& isNewDirectory);
196    bool syncTreeExternal(const char* dir);
197     
198     bool isPathCached(const WaitingTile& next) const;
199     void syncPath(const WaitingTile& next);
200     
201    void initCompletedTilesPersistentCache();
202    void writeCompletedTilesPersistentCache() const;
203    
204 #if defined(SG_SVN_CLIENT)
205    bool syncTreeInternal(const char* dir);
206    bool _use_built_in;
207    HTTP::Client _http;
208    std::auto_ptr<SVNRepository> _repository;
209 #elif defined(HAVE_SVN_CLIENT_H)
210    static int svnClientSetup(void);
211    bool syncTreeInternal(const char* dir);
212
213    bool _use_built_in;
214
215    // Things we need for doing subversion checkout - often
216    static apr_pool_t *_svn_pool;
217    static svn_client_ctx_t *_svn_ctx;
218    static svn_opt_revision_t *_svn_rev;
219    static svn_opt_revision_t *_svn_rev_peg;
220 #endif
221
222    volatile bool _is_dirty;
223    volatile bool _stop;
224    SGBlockingDeque <WaitingTile> waitingTiles;
225    CompletedTiles _completedTiles;
226    SGBlockingDeque <WaitingTile> _freshTiles;
227    bool _use_svn;
228    string _svn_server;
229    string _svn_command;
230    string _rsync_server;
231    string _local_dir;
232    SGPath _persistentCachePath;
233 };
234
235 #ifdef HAVE_SVN_CLIENT_H
236     apr_pool_t* SGTerraSync::SvnThread::_svn_pool = NULL;
237     svn_client_ctx_t* SGTerraSync::SvnThread::_svn_ctx = NULL;
238     svn_opt_revision_t* SGTerraSync::SvnThread::_svn_rev = NULL;
239     svn_opt_revision_t* SGTerraSync::SvnThread::_svn_rev_peg = NULL;
240 #endif
241
242 SGTerraSync::SvnThread::SvnThread() :
243     _active(false),
244     _running(false),
245     _busy(false),
246     _stalled(false),
247     _fail_count(0),
248     _updated_tile_count(0),
249     _success_count(0),
250     _consecutive_errors(0),
251     _allowed_errors(6),
252     _cache_hits(0),
253 #if defined(HAVE_SVN_CLIENT_H) || defined(SG_SVN_CLIENT)
254     _use_built_in(true),
255 #endif
256     _is_dirty(false),
257     _stop(false),
258     _use_svn(true)
259 {
260 #ifdef HAVE_SVN_CLIENT_H
261     int errCode = SGTerraSync::SvnThread::svnClientSetup();
262     if (errCode != EXIT_SUCCESS)
263     {
264         SG_LOG(SG_TERRAIN,SG_ALERT,
265                "Failed to initialize built-in SVN client, error = " << errCode);
266     }
267 #endif
268 }
269
270 void SGTerraSync::SvnThread::stop()
271 {
272     // drop any pending requests
273     waitingTiles.clear();
274
275     if (!_running)
276         return;
277
278     // set stop flag and wake up the thread with an empty request
279     _stop = true;
280     WaitingTile w("",false);
281     request(w);
282     join();
283     _running = false;
284 }
285
286 bool SGTerraSync::SvnThread::start()
287 {
288     if (_running)
289         return false;
290
291     if (_local_dir=="")
292     {
293         SG_LOG(SG_TERRAIN,SG_ALERT,
294                "Cannot start scenery download. Local cache directory is undefined.");
295         _fail_count++;
296         _stalled = true;
297         return false;
298     }
299
300     SGPath path(_local_dir);
301     if (!path.exists())
302     {
303         SG_LOG(SG_TERRAIN,SG_ALERT,
304                "Cannot start scenery download. Directory '" << _local_dir <<
305                "' does not exist. Set correct directory path or create directory folder.");
306         _fail_count++;
307         _stalled = true;
308         return false;
309     }
310
311     path.append("version");
312     if (path.exists())
313     {
314         SG_LOG(SG_TERRAIN,SG_ALERT,
315                "Cannot start scenery download. Directory '" << _local_dir <<
316                "' contains the base package. Use a separate directory.");
317         _fail_count++;
318         _stalled = true;
319         return false;
320     }
321
322 #if defined(HAVE_SVN_CLIENT_H) || defined(SG_SVN_CLIENT)
323     _use_svn |= _use_built_in;
324 #endif
325
326     if ((_use_svn)&&(_svn_server==""))
327     {
328         SG_LOG(SG_TERRAIN,SG_ALERT,
329                "Cannot start scenery download. Subversion scenery server is undefined.");
330         _fail_count++;
331         _stalled = true;
332         return false;
333     }
334     if ((!_use_svn)&&(_rsync_server==""))
335     {
336         SG_LOG(SG_TERRAIN,SG_ALERT,
337                "Cannot start scenery download. Rsync scenery server is undefined.");
338         _fail_count++;
339         _stalled = true;
340         return false;
341     }
342
343     _fail_count = 0;
344     _updated_tile_count = 0;
345     _success_count = 0;
346     _consecutive_errors = 0;
347     _stop = false;
348     _stalled = false;
349     _running = true;
350
351     string status;
352 #if defined(HAVE_SVN_CLIENT_H) || defined(SG_SVN_CLIENT)
353     if (_use_svn && _use_built_in)
354         status = "Using built-in SVN support. ";
355     else
356 #endif
357     if (_use_svn)
358     {
359         status = "Using external SVN utility '";
360         status += _svn_command;
361         status += "'. ";
362     }
363     else
364     {
365         status = "Using RSYNC. ";
366     }
367
368     // not really an alert - but we want to (always) see this message, so user is
369     // aware we're downloading scenery (and using bandwidth).
370     SG_LOG(SG_TERRAIN,SG_ALERT,
371            "Starting automatic scenery download/synchronization. "
372            << status
373            << "Directory: '" << _local_dir << "'.");
374
375     SGThread::start();
376     return true;
377 }
378
379 // sync one directory tree
380 bool SGTerraSync::SvnThread::syncTree(const char* dir, bool& isNewDirectory)
381 {
382     int rc;
383     SGPath path( _local_dir );
384
385     path.append( dir );
386     isNewDirectory = !path.exists();
387     if (isNewDirectory)
388     {
389         rc = path.create_dir( 0755 );
390         if (rc)
391         {
392             SG_LOG(SG_TERRAIN,SG_ALERT,
393                    "Cannot create directory '" << dir << "', return code = " << rc );
394             return false;
395         }
396     }
397
398 #if defined(HAVE_SVN_CLIENT_H) || defined(SG_SVN_CLIENT)
399     if (_use_built_in)
400         return syncTreeInternal(dir);
401     else
402 #endif
403     {
404         return syncTreeExternal(dir);
405     }
406 }
407
408 #if defined(SG_SVN_CLIENT)
409     
410 bool SGTerraSync::SvnThread::syncTreeInternal(const char* dir)
411 {
412     ostringstream command;
413     command << _svn_server << "/" << dir;
414
415     SGPath path(_local_dir);
416     path.append(dir);
417     _repository.reset(new SVNRepository(path, &_http));
418     _repository->setBaseUrl(command.str());
419     
420     SGTimeStamp st;
421     st.stamp();
422     SG_LOG(SG_IO, SG_DEBUG, "terrasync: will sync " << command.str());
423     _repository->update();
424     
425     bool result = true;
426     while (!_stop && _repository->isDoingSync()) {
427         _http.update(100);
428     }
429     
430     if (_repository->failure() == SVNRepository::SVN_ERROR_NOT_FOUND) {
431         // this is fine, but maybe we should use a different return code
432         // in the future to higher layers can distuinguish this case
433     } else if (_repository->failure() != SVNRepository::SVN_NO_ERROR) {
434         result = false;
435     } else {
436         SG_LOG(SG_IO, SG_DEBUG, "sync of " << command.str() << " finished ("
437             << st.elapsedMSec() << " msec");
438     }
439     
440     _repository.reset();
441     return result;
442 }
443     
444 #elif defined(HAVE_SVN_CLIENT_H)
445
446 bool SGTerraSync::SvnThread::syncTreeInternal(const char* dir)
447 {
448     SG_LOG(SG_TERRAIN,SG_DEBUG, "Synchronizing scenery directory " << dir);
449     if (!_svn_pool)
450     {
451         SG_LOG(SG_TERRAIN,SG_ALERT,
452                "Built-in SVN client failed to initialize.");
453         return false;
454     }
455
456     ostringstream command;
457     command << _svn_server << "/" << dir;
458
459     ostringstream dest_base_dir;
460     dest_base_dir << _local_dir << "/" << dir;
461
462     apr_pool_t *subpool = svn_pool_create(_svn_pool);
463
464     svn_error_t *err = NULL;
465 #if (SVN_VER_MINOR >= 5)
466     err = svn_client_checkout3(NULL,
467             command.str().c_str(),
468             dest_base_dir.str().c_str(),
469             _svn_rev_peg,
470             _svn_rev,
471             svn_depth_infinity,
472             0, // ignore-externals = false
473             0, // allow unver obstructions = false
474             _svn_ctx,
475             subpool);
476 #else
477     // version 1.4 API
478     err = svn_client_checkout2(NULL,
479             command.str().c_str(),
480             dest_base_dir.str().c_str(),
481             _svn_rev_peg,
482             _svn_rev,
483             1, // recurse=true - same as svn_depth_infinity for checkout3 above
484             0, // ignore externals = false
485             _svn_ctx,
486             subpool);
487 #endif
488
489     bool ReturnValue = true;
490     if (err)
491     {
492         // Report errors from the checkout attempt
493         if (err->apr_err == SVN_ERR_RA_ILLEGAL_URL)
494         {
495             // ignore errors when remote path doesn't exist (no scenery data for ocean areas)
496         }
497         else
498         {
499             SG_LOG(SG_TERRAIN,SG_ALERT,
500                     "Failed to synchronize directory '" << dir << "', " <<
501                     err->message << " (code " << err->apr_err << ").");
502             svn_error_clear(err);
503             // try to clean up
504             err = svn_client_cleanup(dest_base_dir.str().c_str(),
505                     _svn_ctx,subpool);
506             if (!err)
507             {
508                 SG_LOG(SG_TERRAIN,SG_ALERT,
509                        "SVN repository cleanup successful for '" << dir << "'.");
510             }
511             ReturnValue = false;
512         }
513     } else
514     {
515         SG_LOG(SG_TERRAIN,SG_DEBUG, "Done with scenery directory " << dir);
516     }
517     svn_pool_destroy(subpool);
518     return ReturnValue;
519 }
520 #endif // of HAVE_SVN_CLIENT_H
521
522 bool SGTerraSync::SvnThread::syncTreeExternal(const char* dir)
523 {
524     ostringstream buf;
525     SGPath localPath( _local_dir );
526     localPath.append( dir );
527
528     if (_use_svn)
529     {
530         buf << "\"" << _svn_command << "\" "
531             << svn_options << " "
532             << "\"" << _svn_server << "/" << dir << "\" "
533             << "\"" << localPath.str_native() << "\"";
534     } else {
535         buf << rsync_cmd << " "
536             << "\"" << _rsync_server << "/" << dir << "/\" "
537             << "\"" << localPath.str_native() << "/\"";
538     }
539
540     string command;
541 #ifdef SG_WINDOWS
542         // windows command line parsing is just lovely...
543         // to allow white spaces, the system call needs this:
544         // ""C:\Program Files\something.exe" somearg "some other arg""
545         // Note: whitespace strings quoted by a pair of "" _and_ the 
546         //       entire string needs to be wrapped by "" too.
547         // The svn url needs forward slashes (/) as a path separator while
548         // the local path needs windows-native backslash as a path separator.
549     command = "\"" + buf.str() + "\"";
550 #else
551     command = buf.str();
552 #endif
553     SG_LOG(SG_TERRAIN,SG_DEBUG, "sync command '" << command << "'");
554
555 #ifdef SG_WINDOWS
556     // tbd: does Windows support "popen"?
557     int rc = system( command.c_str() );
558 #else
559     FILE* pipe = popen( command.c_str(), "r");
560     int rc=-1;
561     // wait for external process to finish
562     if (pipe)
563         rc = pclose(pipe);
564 #endif
565
566     if (rc)
567     {
568         SG_LOG(SG_TERRAIN,SG_ALERT,
569                "Failed to synchronize directory '" << dir << "', " <<
570                "error code= " << rc);
571         return false;
572     }
573     return true;
574 }
575
576 void SGTerraSync::SvnThread::run()
577 {
578     _active = true;
579     
580     initCompletedTilesPersistentCache();
581     
582     while (!_stop)
583     {
584         WaitingTile next = waitingTiles.pop_front();
585         if (_stop)
586            break;
587
588         if (isPathCached(next)) {
589             _cache_hits++;
590             SG_LOG(SG_TERRAIN, SG_DEBUG,
591                    "Cache hit for: '" << next._dir << "'");
592             continue;
593         }
594         
595         syncPath(next);
596
597         if ((_allowed_errors >= 0)&&
598             (_consecutive_errors >= _allowed_errors))
599         {
600             _stalled = true;
601             _stop = true;
602         }
603     }
604
605     _active = false;
606     _running = false;
607     _is_dirty = true;
608 }
609
610 bool SGTerraSync::SvnThread::isPathCached(const WaitingTile& next) const
611 {
612     CompletedTiles::const_iterator ii = _completedTiles.find( next._dir );
613     if (ii == _completedTiles.end()) {
614         return false;
615     }
616     
617     // check if the path still physically exists
618     SGPath p(_local_dir);
619     p.append(next._dir);
620     if (!p.exists()) {
621         return false;
622     }
623     
624     time_t now = time(0);
625     return (ii->second > now);
626 }
627
628 void SGTerraSync::SvnThread::syncPath(const WaitingTile& next)
629 {
630     bool isNewDirectory = false;
631     time_t now = time(0);
632     
633     _busy = true;
634     if (!syncTree(next._dir.c_str(),isNewDirectory))
635     {
636         _consecutive_errors++;
637         _fail_count++;
638         _completedTiles[ next._dir ] = now + UpdateInterval::FailedAttempt;
639     }
640     else
641     {
642         _consecutive_errors = 0;
643         _success_count++;
644         SG_LOG(SG_TERRAIN,SG_INFO,
645                "Successfully synchronized directory '" << next._dir << "'");
646         if (next._refreshScenery)
647         {
648             // updated a tile
649             _updated_tile_count++;
650             if (isNewDirectory)
651             {
652                 // for now only report new directories to refresh display
653                 // (i.e. only when ocean needs to be replaced with actual data)
654                 _freshTiles.push_back(next);
655                 _is_dirty = true;
656             }
657         }
658         
659         _completedTiles[ next._dir ] = now + UpdateInterval::SuccessfulAttempt;
660         writeCompletedTilesPersistentCache();
661     }
662     _busy = false;
663 }
664
665 #if defined(HAVE_SVN_CLIENT_H)
666 // Configure our subversion session
667 int SGTerraSync::SvnThread::svnClientSetup(void)
668 {
669     // Are we already prepared?
670     if (_svn_pool) return EXIT_SUCCESS;
671     // No, so initialize svn internals generally
672
673 #ifdef _MSC_VER
674     // there is a segfault when providing an error stream.
675     //  Apparently, calling setvbuf with a nul buffer is
676     //  not supported under msvc 7.1 ( code inside svn_cmdline_init )
677     if (svn_cmdline_init("terrasync", 0) != EXIT_SUCCESS)
678         return EXIT_FAILURE;
679
680     // revert locale setting
681     setlocale(LC_ALL,"C");
682 #else
683     /* svn_cmdline_init configures the locale. Setup environment to ensure the
684      * default "C" locale remains active, since fgfs isn't locale aware - especially
685      * requires "." as decimal point in strings containing floating point varibales. */
686     setenv("LC_ALL", "C", 1);
687
688     if (svn_cmdline_init("terrasync", stderr) != EXIT_SUCCESS)
689         return EXIT_FAILURE;
690 #endif
691
692     apr_pool_t *pool = NULL;
693     
694     apr_allocator_t* allocator = NULL;
695     int aprErr = apr_allocator_create(&allocator);
696     if (aprErr != APR_SUCCESS)
697         return EXIT_FAILURE;
698     
699     apr_pool_create_ex(&pool, NULL /* parent pool */, NULL /* abort func */, allocator);
700     
701     svn_error_t *err = NULL;
702     SVN_VERSION_DEFINE(_svn_version);
703     err = svn_ver_check_list(&_svn_version, mysvn_checklist);
704     if (err)
705         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
706     err = svn_ra_initialize(pool);
707     if (err)
708         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
709     char *config_dir = NULL;
710     err = svn_config_ensure(config_dir, pool);
711     if (err)
712         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
713     err = svn_client_create_context(&_svn_ctx, pool);
714     if (err)
715         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
716     err = svn_config_get_config(&(_svn_ctx->config),
717         config_dir, pool);
718     if (err)
719         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
720     svn_config_t *cfg;
721     cfg = ( svn_config_t*) apr_hash_get(
722         _svn_ctx->config,
723         SVN_CONFIG_CATEGORY_CONFIG,
724         APR_HASH_KEY_STRING);
725     if (err)
726         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
727
728     svn_auth_baton_t *ab=NULL;
729
730 #if (SVN_VER_MINOR >= 6)
731     err = svn_cmdline_create_auth_baton  (&ab,
732             TRUE, NULL, NULL, config_dir, TRUE, FALSE, cfg,
733             _svn_ctx->cancel_func, _svn_ctx->cancel_baton, pool);
734 #else
735     err = svn_cmdline_setup_auth_baton(&ab,
736         TRUE, NULL, NULL, config_dir, TRUE, cfg,
737         _svn_ctx->cancel_func, _svn_ctx->cancel_baton, pool);
738 #endif
739
740     if (err)
741         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
742     
743     _svn_ctx->auth_baton = ab;
744 #if (SVN_VER_MINOR >= 5)
745     _svn_ctx->conflict_func = NULL;
746     _svn_ctx->conflict_baton = NULL;
747 #endif
748
749     // Now our magic revisions
750     _svn_rev = (svn_opt_revision_t*) apr_palloc(pool, 
751         sizeof(svn_opt_revision_t));
752     if (!_svn_rev)
753         return EXIT_FAILURE;
754     _svn_rev_peg = (svn_opt_revision_t*) apr_palloc(pool, 
755         sizeof(svn_opt_revision_t));
756     if (!_svn_rev_peg)
757         return EXIT_FAILURE;
758     _svn_rev->kind = svn_opt_revision_head;
759     _svn_rev_peg->kind = svn_opt_revision_unspecified;
760     // Success if we got this far
761     _svn_pool = pool;
762     return EXIT_SUCCESS;
763 }
764 #endif // of defined(HAVE_SVN_CLIENT_H)
765
766 void SGTerraSync::SvnThread::initCompletedTilesPersistentCache()
767 {
768     if (!_persistentCachePath.exists()) {
769         return;
770     }
771     
772     SGPropertyNode_ptr cacheRoot(new SGPropertyNode);
773     time_t now = time(0);
774     
775     readProperties(_persistentCachePath.str(), cacheRoot);
776     for (unsigned int i=0; i<cacheRoot->nChildren(); ++i) {
777         SGPropertyNode* entry = cacheRoot->getChild(i);
778         string tileName = entry->getStringValue("path");
779         time_t stamp = entry->getIntValue("stamp");
780         if (stamp < now) {
781             continue;
782         }
783         
784         _completedTiles[tileName] = stamp;
785     }
786 }
787
788 void SGTerraSync::SvnThread::writeCompletedTilesPersistentCache() const
789 {
790     // cache is disabled
791     if (_persistentCachePath.isNull()) {
792         return;
793     }
794     
795     std::ofstream f(_persistentCachePath.c_str(), std::ios::trunc);
796     if (!f.is_open()) {
797         return;
798     }
799     
800     SGPropertyNode_ptr cacheRoot(new SGPropertyNode);
801     CompletedTiles::const_iterator it = _completedTiles.begin();
802     for (; it != _completedTiles.end(); ++it) {
803         SGPropertyNode* entry = cacheRoot->addChild("entry");
804         entry->setStringValue("path", it->first);
805         entry->setIntValue("stamp", it->second);
806     }
807     
808     writeProperties(f, cacheRoot, true /* write_all */);
809     f.close();
810 }
811
812 ///////////////////////////////////////////////////////////////////////////////
813 // SGTerraSync ////////////////////////////////////////////////////////////////
814 ///////////////////////////////////////////////////////////////////////////////
815 SGTerraSync::SGTerraSync(SGPropertyNode_ptr root) :
816     _svnThread(NULL),
817     last_lat(NOWHERE),
818     last_lon(NOWHERE),
819     _terraRoot(root->getNode("/sim/terrasync",true)),
820     _refreshCb(NULL),
821     _userCbData(NULL)
822 {
823     _svnThread = new SvnThread();
824     _log = new BufferedLogCallback(SG_TERRAIN, SG_INFO);
825     _log->truncateAt(255);
826     
827     sglog().addCallback(_log);
828 }
829
830 SGTerraSync::~SGTerraSync()
831 {
832     _tiedProperties.Untie();
833     delete _svnThread;
834     _svnThread = NULL;
835     sglog().removeCallback(_log);
836     delete _log;
837 }
838
839 void SGTerraSync::init()
840 {
841     _refreshDisplay = _terraRoot->getNode("refresh-display",true);
842     _terraRoot->setBoolValue("built-in-svn-available",svn_built_in_available);
843     reinit();
844 }
845
846 void SGTerraSync::reinit()
847 {
848     // do not reinit when enabled and we're already up and running
849     if ((_terraRoot->getBoolValue("enabled",false))&&
850          (_svnThread->_active && _svnThread->_running))
851         return;
852
853     _svnThread->stop();
854
855     if (_terraRoot->getBoolValue("enabled",false))
856     {
857         _svnThread->setSvnServer(_terraRoot->getStringValue("svn-server",""));
858         _svnThread->setRsyncServer(_terraRoot->getStringValue("rsync-server",""));
859         _svnThread->setLocalDir(_terraRoot->getStringValue("scenery-dir",""));
860         _svnThread->setAllowedErrorCount(_terraRoot->getIntValue("max-errors",5));
861         _svnThread->setCachePath(SGPath(_terraRoot->getStringValue("cache-path","")));
862         _svnThread->setCacheHits(_terraRoot->getIntValue("cache-hit", 0));
863         
864     #if defined(HAVE_SVN_CLIENT_H) || defined(SG_SVN_CLIENT)
865         _svnThread->setUseBuiltin(_terraRoot->getBoolValue("use-built-in-svn",true));
866     #else
867         _terraRoot->setBoolValue("use-built-in-svn",false);
868     #endif
869         _svnThread->setUseSvn(_terraRoot->getBoolValue("use-svn",true));
870         _svnThread->setExtSvnUtility(_terraRoot->getStringValue("ext-svn-utility","svn"));
871
872         if (_svnThread->start())
873         {
874             syncAirportsModels();
875             if (last_lat != NOWHERE && last_lon != NOWHERE)
876             {
877                 // reschedule most recent position
878                 int lat = last_lat;
879                 int lon = last_lon;
880                 last_lat = NOWHERE;
881                 last_lon = NOWHERE;
882                 schedulePosition(lat, lon);
883             }
884         }
885     }
886
887     _stalledNode->setBoolValue(_svnThread->_stalled);
888 }
889
890 void SGTerraSync::bind()
891 {
892     _tiedProperties.Tie( _terraRoot->getNode("busy", true), (bool*) &_svnThread->_busy );
893     _tiedProperties.Tie( _terraRoot->getNode("active", true), (bool*) &_svnThread->_active );
894     _tiedProperties.Tie( _terraRoot->getNode("update-count", true), (int*) &_svnThread->_success_count );
895     _tiedProperties.Tie( _terraRoot->getNode("error-count", true), (int*) &_svnThread->_fail_count );
896     _tiedProperties.Tie( _terraRoot->getNode("tile-count", true), (int*) &_svnThread->_updated_tile_count );
897     _tiedProperties.Tie( _terraRoot->getNode("cache-hits", true), (int*) &_svnThread->_cache_hits );
898     
899     _terraRoot->getNode("busy", true)->setAttribute(SGPropertyNode::WRITE,false);
900     _terraRoot->getNode("active", true)->setAttribute(SGPropertyNode::WRITE,false);
901     _terraRoot->getNode("update-count", true)->setAttribute(SGPropertyNode::WRITE,false);
902     _terraRoot->getNode("error-count", true)->setAttribute(SGPropertyNode::WRITE,false);
903     _terraRoot->getNode("tile-count", true)->setAttribute(SGPropertyNode::WRITE,false);
904     _terraRoot->getNode("use-built-in-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
905     _terraRoot->getNode("use-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
906     // stalled is used as a signal handler (to connect listeners triggering GUI pop-ups)
907     _stalledNode = _terraRoot->getNode("stalled", true);
908     _stalledNode->setBoolValue(_svnThread->_stalled);
909     _stalledNode->setAttribute(SGPropertyNode::PRESERVE,true);
910 }
911
912 void SGTerraSync::unbind()
913 {
914     _svnThread->stop();
915     _tiedProperties.Untie();
916 }
917
918 void SGTerraSync::update(double)
919 {
920     static SGBucket bucket;
921     if (_svnThread->isDirty())
922     {
923         if (!_svnThread->_active)
924         {
925             if (_svnThread->_stalled)
926             {
927                 SG_LOG(SG_TERRAIN,SG_ALERT,
928                        "Automatic scenery download/synchronization stalled. Too many errors.");
929             }
930             else
931             {
932                 // not really an alert - just always show this message
933                 SG_LOG(SG_TERRAIN,SG_ALERT,
934                         "Automatic scenery download/synchronization has stopped.");
935             }
936             _stalledNode->setBoolValue(_svnThread->_stalled);
937         }
938
939         if (!_refreshDisplay->getBoolValue())
940             return;
941
942         while (_svnThread->hasNewTiles())
943         {
944             WaitingTile next = _svnThread->getNewTile();
945             if (next._refreshScenery)
946             {
947                 refreshScenery(_svnThread->getLocalDir(),next._dir);
948             }
949         }
950     }
951 }
952
953 void SGTerraSync::refreshScenery(SGPath path,const string& relativeDir)
954 {
955     // find tiles to be refreshed
956     if (_refreshCb)
957     {
958         path.append(relativeDir);
959         if (path.exists())
960         {
961             simgear::Dir dir(path);
962             //TODO need to be smarter here. only update tiles which actually
963             // changed recently. May also be possible to use information from the
964             // built-in SVN client directly (instead of checking directory contents).
965             PathList tileList = dir.children(simgear::Dir::TYPE_FILE, ".stg");
966             for (unsigned int i=0; i<tileList.size(); ++i)
967             {
968                 // reload scenery tile
969                 long index = atoi(tileList[i].file().c_str());
970                 _refreshCb(_userCbData, index);
971             }
972         }
973     }
974 }
975
976 bool SGTerraSync::isIdle() {return _svnThread->isIdle();}
977
978 void SGTerraSync::setTileRefreshCb(SGTerraSyncCallback refreshCb, void* userCbData)
979 {
980     _refreshCb = refreshCb;
981     _userCbData = userCbData;
982 }
983
984 void SGTerraSync::syncAirportsModels()
985 {
986     static const char* bounds = "MZAJKL"; // airport sync order: K-L, A-J, M-Z
987     // note "request" method uses LIFO order, i.e. processes most recent request first
988     for( unsigned i = 0; i < strlen(bounds)/2; i++ )
989     {
990         for ( char synced_other = bounds[2*i]; synced_other <= bounds[2*i+1]; synced_other++ )
991         {
992             ostringstream dir;
993             dir << "Airports/" << synced_other;
994             WaitingTile w(dir.str(),false);
995             _svnThread->request( w );
996         }
997     }
998     WaitingTile w("Models",false);
999     _svnThread->request( w );
1000 }
1001
1002
1003 void SGTerraSync::syncArea( int lat, int lon )
1004 {
1005     if ( lat < -90 || lat > 90 || lon < -180 || lon > 180 )
1006         return;
1007     char NS, EW;
1008     int baselat, baselon;
1009
1010     if ( lat < 0 ) {
1011         int base = (int)(lat / 10);
1012         if ( lat == base * 10 ) {
1013             baselat = base * 10;
1014         } else {
1015             baselat = (base - 1) * 10;
1016         }
1017         NS = 's';
1018     } else {
1019         baselat = (int)(lat / 10) * 10;
1020         NS = 'n';
1021     }
1022     if ( lon < 0 ) {
1023         int base = (int)(lon / 10);
1024         if ( lon == base * 10 ) {
1025             baselon = base * 10;
1026         } else {
1027             baselon = (base - 1) * 10;
1028         }
1029         EW = 'w';
1030     } else {
1031         baselon = (int)(lon / 10) * 10;
1032         EW = 'e';
1033     }
1034
1035     const char* terrainobjects[3] = { "Terrain", "Objects",  0 };
1036     bool refresh=true;
1037     for (const char** tree = &terrainobjects[0]; *tree; tree++)
1038     {
1039         ostringstream dir;
1040         dir << *tree << "/" << setfill('0')
1041             << EW << setw(3) << abs(baselon) << NS << setw(2) << abs(baselat) << "/"
1042             << EW << setw(3) << abs(lon)     << NS << setw(2) << abs(lat);
1043         WaitingTile w(dir.str(),refresh);
1044         _svnThread->request( w );
1045         refresh=false;
1046     }
1047 }
1048
1049
1050 void SGTerraSync::syncAreas( int lat, int lon, int lat_dir, int lon_dir )
1051 {
1052     if ( lat_dir == 0 && lon_dir == 0 ) {
1053         // do surrounding 8 1x1 degree areas.
1054         for ( int i = lat - 1; i <= lat + 1; ++i ) {
1055             for ( int j = lon - 1; j <= lon + 1; ++j ) {
1056                 if ( i != lat || j != lon ) {
1057                     syncArea( i, j );
1058                 }
1059             }
1060         }
1061     } else {
1062         if ( lat_dir != 0 ) {
1063             syncArea( lat + lat_dir, lon - 1 );
1064             syncArea( lat + lat_dir, lon + 1 );
1065             syncArea( lat + lat_dir, lon );
1066         }
1067         if ( lon_dir != 0 ) {
1068             syncArea( lat - 1, lon + lon_dir );
1069             syncArea( lat + 1, lon + lon_dir );
1070             syncArea( lat, lon + lon_dir );
1071         }
1072     }
1073
1074     // do current 1x1 degree area first
1075     syncArea( lat, lon );
1076 }
1077
1078
1079 bool SGTerraSync::schedulePosition(int lat, int lon)
1080 {
1081     bool Ok = false;
1082
1083     // Ignore messages where the location does not change
1084     if ( lat != last_lat || lon != last_lon )
1085     {
1086         if (_svnThread->_running)
1087         {
1088             SG_LOG(SG_TERRAIN,SG_DEBUG, "Requesting scenery update for position " <<
1089                                         lat << "," << lon);
1090             int lat_dir=0;
1091             int lon_dir=0;
1092             if ( last_lat != NOWHERE && last_lon != NOWHERE )
1093             {
1094                 int dist = lat - last_lat;
1095                 if ( dist != 0 )
1096                 {
1097                     lat_dir = dist / abs(dist);
1098                 }
1099                 else
1100                 {
1101                     lat_dir = 0;
1102                 }
1103                 dist = lon - last_lon;
1104                 if ( dist != 0 )
1105                 {
1106                     lon_dir = dist / abs(dist);
1107                 } else
1108                 {
1109                     lon_dir = 0;
1110                 }
1111             }
1112
1113             SG_LOG(SG_TERRAIN,SG_DEBUG, "Scenery update for " <<
1114                    "lat = " << lat << ", lon = " << lon <<
1115                    ", lat_dir = " << lat_dir << ",  " <<
1116                    "lon_dir = " << lon_dir);
1117
1118             syncAreas( lat, lon, lat_dir, lon_dir );
1119             Ok = true;
1120         }
1121         last_lat = lat;
1122         last_lon = lon;
1123     }
1124
1125     return Ok;
1126 }
1127