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