1 // terrasync.cxx -- scenery fetcher
3 // Started by Curtis Olson, November 2002.
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>
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.
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.
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.
26 # include <simgear_config.h>
29 #include <simgear/compiler.h>
38 #elif defined(_MSC_VER)
40 # ifndef HAVE_SVN_CLIENT_H
46 #include <stdlib.h> // atoi() atof() abs() system()
47 #include <signal.h> // signal()
54 #include <simgear/compiler.h>
56 #include "terrasync.hxx"
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/scene/tgdb/TileCache.hxx>
62 #include <simgear/misc/sg_dir.hxx>
63 #include <OpenThreads/Thread>
65 #ifdef HAVE_SVN_CLIENT_H
66 # ifdef HAVE_LIBSVN_CLIENT_1
67 # include <svn_version.h>
68 # include <svn_auth.h>
69 # include <svn_client.h>
70 # include <svn_cmdline.h>
71 # include <svn_pools.h>
73 # undef HAVE_SVN_CLIENT_H
77 #ifdef HAVE_SVN_CLIENT_H
78 static const svn_version_checklist_t mysvn_checklist[] = {
79 { "svn_subr", svn_subr_version },
80 { "svn_client", svn_client_version },
83 static const bool svn_built_in_available = true;
85 static const bool svn_built_in_available = false;
88 using namespace simgear;
90 const char* rsync_cmd =
91 "rsync --verbose --archive --delete --perms --owner --group";
93 const char* svn_options =
96 namespace UpdateInterval
98 // interval in seconds to allow an update to repeat after a successful update (=daily)
99 static const double SuccessfulAttempt = 24*60*60;
100 // interval in seconds to allow another update after a failed attempt (10 minutes)
101 static const double FailedAttempt = 10*60;
104 typedef map<string,time_t> CompletedTiles;
106 ///////////////////////////////////////////////////////////////////////////////
107 // helper functions ///////////////////////////////////////////////////////////
108 ///////////////////////////////////////////////////////////////////////////////
109 string stripPath(string path)
111 // svn doesn't like trailing white-spaces or path separators - strip them!
112 path = simgear::strutils::strip(path);
113 int slen = path.length();
115 ((path[slen-1]=='/')||(path[slen-1]=='\\')))
119 return path.substr(0,slen);
122 ///////////////////////////////////////////////////////////////////////////////
123 // WaitingTile ////////////////////////////////////////////////////////////////
124 ///////////////////////////////////////////////////////////////////////////////
128 WaitingTile(string dir,bool refresh) :
129 _dir(dir), _refreshScenery(refresh) {}
131 bool _refreshScenery;
134 ///////////////////////////////////////////////////////////////////////////////
135 // SGTerraSync::SvnThread /////////////////////////////////////////////////////
136 ///////////////////////////////////////////////////////////////////////////////
137 class SGTerraSync::SvnThread : public OpenThreads::Thread
141 virtual ~SvnThread( ) { stop(); }
146 bool isIdle() {return waitingTiles.empty();}
147 void request(const WaitingTile& dir) {waitingTiles.push_front(dir);}
148 bool isDirty() { bool r = _is_dirty;_is_dirty = false;return r;}
149 bool hasNewTiles() { return !_freshTiles.empty();}
150 WaitingTile getNewTile() { return _freshTiles.pop_front();}
152 void setSvnServer(string server) { _svn_server = stripPath(server);}
153 void setExtSvnUtility(string svn_util) { _svn_command = simgear::strutils::strip(svn_util);}
154 void setRsyncServer(string server) { _rsync_server = simgear::strutils::strip(server);}
155 void setLocalDir(string dir) { _local_dir = stripPath(dir);}
156 string getLocalDir() { return _local_dir;}
157 void setUseSvn(bool use_svn) { _use_svn = use_svn;}
159 #ifdef HAVE_SVN_CLIENT_H
160 void setUseBuiltin(bool built_in) { _use_built_in = built_in;}
163 volatile bool _active;
164 volatile bool _running;
166 volatile bool _stalled;
167 volatile int _fail_count;
168 volatile int _updated_tile_count;
169 volatile int _success_count;
170 volatile int _consecutive_errors;
174 bool syncTree(const char* dir, bool& isNewDirectory);
175 bool syncTreeExternal(const char* dir);
177 #ifdef HAVE_SVN_CLIENT_H
178 static int svnClientSetup(void);
179 bool syncTreeInternal(const char* dir);
183 // Things we need for doing subversion checkout - often
184 static apr_pool_t *_svn_pool;
185 static svn_client_ctx_t *_svn_ctx;
186 static svn_opt_revision_t *_svn_rev;
187 static svn_opt_revision_t *_svn_rev_peg;
190 volatile bool _is_dirty;
192 SGBlockingDeque <WaitingTile> waitingTiles;
193 CompletedTiles _completedTiles;
194 SGBlockingDeque <WaitingTile> _freshTiles;
198 string _rsync_server;
202 #ifdef HAVE_SVN_CLIENT_H
203 apr_pool_t* SGTerraSync::SvnThread::_svn_pool = NULL;
204 svn_client_ctx_t* SGTerraSync::SvnThread::_svn_ctx = NULL;
205 svn_opt_revision_t* SGTerraSync::SvnThread::_svn_rev = NULL;
206 svn_opt_revision_t* SGTerraSync::SvnThread::_svn_rev_peg = NULL;
209 SGTerraSync::SvnThread::SvnThread() :
215 _updated_tile_count(0),
217 _consecutive_errors(0),
218 #ifdef HAVE_SVN_CLIENT_H
225 #ifdef HAVE_SVN_CLIENT_H
226 int errCode = SGTerraSync::SvnThread::svnClientSetup();
227 if (errCode != EXIT_SUCCESS)
229 SG_LOG(SG_TERRAIN,SG_ALERT,
230 "Failed to initialize built-in SVN client, error = " << errCode);
235 void SGTerraSync::SvnThread::stop()
237 // drop any pending requests
238 waitingTiles.clear();
243 // set stop flag and wake up the thread with an empty request
245 WaitingTile w("",false);
251 bool SGTerraSync::SvnThread::start()
258 SG_LOG(SG_TERRAIN,SG_ALERT,
259 "Cannot start scenery download. Local cache directory is undefined.");
265 SGPath path(_local_dir);
268 SG_LOG(SG_TERRAIN,SG_ALERT,
269 "Cannot start scenery download. Directory '" << _local_dir <<
270 "' does not exist. Set correct directory path or create directory folder.");
276 path.append("version");
279 SG_LOG(SG_TERRAIN,SG_ALERT,
280 "Cannot start scenery download. Directory '" << _local_dir <<
281 "' contains the base package. Use a separate directory.");
286 #ifdef HAVE_SVN_CLIENT_H
287 _use_svn |= _use_built_in;
290 if ((_use_svn)&&(_svn_server==""))
292 SG_LOG(SG_TERRAIN,SG_ALERT,
293 "Cannot start scenery download. Subversion scenery server is undefined.");
299 if ((!_use_svn)&&(_rsync_server==""))
301 SG_LOG(SG_TERRAIN,SG_ALERT,
302 "Cannot start scenery download. Rsync scenery server is undefined.");
309 _updated_tile_count = 0;
311 _consecutive_errors = 0;
317 #ifdef HAVE_SVN_CLIENT_H
318 if (_use_svn && _use_built_in)
319 status = "Using built-in SVN support. ";
324 status = "Using external SVN utility '";
325 status += _svn_command;
330 status = "Using RSYNC. ";
333 // not really an alert - but we want to (always) see this message, so user is
334 // aware we're downloading scenery (and using bandwidth).
335 SG_LOG(SG_TERRAIN,SG_ALERT,
336 "Starting automatic scenery download/synchronization. "
338 << "Directory: '" << _local_dir << "'.");
340 OpenThreads::Thread::start();
344 // sync one directory tree
345 bool SGTerraSync::SvnThread::syncTree(const char* dir, bool& isNewDirectory)
348 SGPath path( _local_dir );
351 isNewDirectory = !path.exists();
354 rc = path.create_dir( 0755 );
357 SG_LOG(SG_TERRAIN,SG_ALERT,
358 "Cannot create directory '" << dir << "', return code = " << rc );
363 #ifdef HAVE_SVN_CLIENT_H
365 return syncTreeInternal(dir);
369 return syncTreeExternal(dir);
374 #ifdef HAVE_SVN_CLIENT_H
375 bool SGTerraSync::SvnThread::syncTreeInternal(const char* dir)
377 SG_LOG(SG_TERRAIN,SG_DEBUG, "Synchronizing scenery directory " << dir);
380 SG_LOG(SG_TERRAIN,SG_ALERT,
381 "Built-in SVN client failed to initialize.");
386 char dest_base_dir[512];
387 snprintf( command, 512,
388 "%s/%s", _svn_server.c_str(), dir);
389 snprintf( dest_base_dir, 512,
390 "%s/%s", _local_dir.c_str(), dir);
392 apr_pool_t *subpool = svn_pool_create(_svn_pool);
394 svn_error_t *err = NULL;
395 #if (SVN_VER_MINOR >= 5)
396 err = svn_client_checkout3(NULL,
402 0, // ignore-externals = false
403 0, // allow unver obstructions = false
408 err = svn_client_checkout2(NULL,
413 1, // recurse=true - same as svn_depth_infinity for checkout3 above
414 0, // ignore externals = false
419 bool ReturnValue = true;
422 // Report errors from the checkout attempt
423 if (err->apr_err == SVN_ERR_RA_ILLEGAL_URL)
425 // ignore errors when remote path doesn't exist (no scenery data for ocean areas)
429 SG_LOG(SG_TERRAIN,SG_ALERT,
430 "Failed to synchronize directory '" << dir << "', " <<
431 err->message << " (code " << err->apr_err << ").");
432 svn_error_clear(err);
434 err = svn_client_cleanup(dest_base_dir,
438 SG_LOG(SG_TERRAIN,SG_ALERT,
439 "SVN repository cleanup successful for '" << dir << "'.");
445 SG_LOG(SG_TERRAIN,SG_DEBUG, "Done with scenery directory " << dir);
447 svn_pool_destroy(subpool);
452 bool SGTerraSync::SvnThread::syncTreeExternal(const char* dir)
458 snprintf( command, 512,
459 "\"%s\" %s %s/%s \"%s/%s\"", _svn_command.c_str(), svn_options,
460 _svn_server.c_str(), dir,
461 _local_dir.c_str(), dir );
463 snprintf( command, 512,
464 "%s %s/%s/ \"%s/%s/\"", rsync_cmd,
465 _rsync_server.c_str(), dir,
466 _local_dir.c_str(), dir );
468 SG_LOG(SG_TERRAIN,SG_DEBUG, "sync command '" << command << "'");
469 rc = system( command );
472 SG_LOG(SG_TERRAIN,SG_ALERT,
473 "Failed to synchronize directory '" << dir << "', " <<
474 "error code= " << rc);
480 void SGTerraSync::SvnThread::run()
485 WaitingTile next = waitingTiles.pop_front();
489 CompletedTiles::iterator ii =
490 _completedTiles.find( next._dir );
491 time_t now = time(0);
492 if ((ii == _completedTiles.end())||
495 bool isNewDirectory = false;
498 if (!syncTree(next._dir.c_str(),isNewDirectory))
500 _consecutive_errors++;
502 _completedTiles[ next._dir ] = now + UpdateInterval::FailedAttempt;
506 _consecutive_errors = 0;
508 SG_LOG(SG_TERRAIN,SG_INFO,
509 "Successfully synchronized directory '" << next._dir << "'");
510 if (next._refreshScenery)
513 _updated_tile_count++;
516 // for now only report new directories to refresh display
517 // (i.e. only when ocean needs to be replaced with actual data)
518 _freshTiles.push_back(next);
522 _completedTiles[ next._dir ] = now + UpdateInterval::SuccessfulAttempt;
527 if (_consecutive_errors >= 5)
539 #ifdef HAVE_SVN_CLIENT_H
540 // Configure our subversion session
541 int SGTerraSync::SvnThread::svnClientSetup(void)
543 // Are we already prepared?
544 if (_svn_pool) return EXIT_SUCCESS;
545 // No, so initialize svn internals generally
548 // there is a segfault when providing an error stream.
549 // Apparently, calling setvbuf with a nul buffer is
550 // not supported under msvc 7.1 ( code inside svn_cmdline_init )
551 if (svn_cmdline_init("terrasync", 0) != EXIT_SUCCESS)
554 if (svn_cmdline_init("terrasync", stderr) != EXIT_SUCCESS)
557 /* Oh no! svn_cmdline_init configures the locale - affecting numeric output
558 * formats (i.e. sprintf("%f", ...)). fgfs relies on "C" locale in many places
559 * (including assumptions on required sprintf buffer sizes). Things go horribly
560 * wrong when the locale is changed to anything else but "C". Might be enough to
561 * revert LC_NUMERIC locale - but we'll do a complete revert for now...*/
562 setlocale(LC_ALL,"C");
565 apr_pool_create(&pool, NULL);
566 svn_error_t *err = NULL;
567 SVN_VERSION_DEFINE(_svn_version);
568 err = svn_ver_check_list(&_svn_version, mysvn_checklist);
570 return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
571 err = svn_ra_initialize(pool);
573 return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
574 char *config_dir = NULL;
575 err = svn_config_ensure(config_dir, pool);
577 return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
578 err = svn_client_create_context(&_svn_ctx, pool);
580 return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
581 err = svn_config_get_config(&(_svn_ctx->config),
584 return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
586 cfg = ( svn_config_t*) apr_hash_get(
588 SVN_CONFIG_CATEGORY_CONFIG,
589 APR_HASH_KEY_STRING);
591 return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
593 svn_auth_baton_t *ab=NULL;
595 #if (SVN_VER_MINOR >= 6)
596 err = svn_cmdline_create_auth_baton (&ab,
597 TRUE, NULL, NULL, config_dir, TRUE, FALSE, cfg,
598 _svn_ctx->cancel_func, _svn_ctx->cancel_baton, pool);
600 err = svn_cmdline_setup_auth_baton(&ab,
601 TRUE, NULL, NULL, config_dir, TRUE, cfg,
602 _svn_ctx->cancel_func, _svn_ctx->cancel_baton, pool);
606 return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
608 _svn_ctx->auth_baton = ab;
609 #if (SVN_VER_MINOR >= 5)
610 _svn_ctx->conflict_func = NULL;
611 _svn_ctx->conflict_baton = NULL;
614 // Now our magic revisions
615 _svn_rev = (svn_opt_revision_t*) apr_palloc(pool,
616 sizeof(svn_opt_revision_t));
619 _svn_rev_peg = (svn_opt_revision_t*) apr_palloc(pool,
620 sizeof(svn_opt_revision_t));
623 _svn_rev->kind = svn_opt_revision_head;
624 _svn_rev_peg->kind = svn_opt_revision_unspecified;
625 // Success if we got this far
631 ///////////////////////////////////////////////////////////////////////////////
632 // SGTerraSync ////////////////////////////////////////////////////////////////
633 ///////////////////////////////////////////////////////////////////////////////
634 SGTerraSync::SGTerraSync(SGPropertyNode_ptr root) :
638 _terraRoot(root->getNode("/sim/terrasync",true)),
641 _svnThread = new SvnThread();
644 SGTerraSync::~SGTerraSync()
646 _tiedProperties.Untie();
651 void SGTerraSync::init()
653 _refresh_display = _terraRoot->getNode("refresh-display",true);
654 _terraRoot->getNode("built-in-svn-available",true)->setBoolValue(svn_built_in_available);
658 void SGTerraSync::reinit()
660 // do not reinit when enabled and we're already up and running
661 if ((_terraRoot->getBoolValue("enabled",false))&&
662 (_svnThread->_active && _svnThread->_running))
667 if (_terraRoot->getBoolValue("enabled",false))
669 _svnThread->setSvnServer(_terraRoot->getStringValue("svn-server",""));
670 _svnThread->setRsyncServer(_terraRoot->getStringValue("rsync-server",""));
671 _svnThread->setLocalDir(_terraRoot->getStringValue("scenery-dir",""));
673 #ifdef HAVE_SVN_CLIENT_H
674 _svnThread->setUseBuiltin(_terraRoot->getBoolValue("use-built-in-svn",true));
676 _terraRoot->getNode("use-built-in-svn",true)->setBoolValue(false);
678 _svnThread->setUseSvn(_terraRoot->getBoolValue("use-svn",true));
679 _svnThread->setExtSvnUtility(_terraRoot->getStringValue("ext-svn-utility","svn"));
681 if (_svnThread->start())
682 syncAirportsModels();
685 _stalled_node->setBoolValue(_svnThread->_stalled);
690 void SGTerraSync::bind()
692 _tiedProperties.Tie( _terraRoot->getNode("busy", true), (bool*) &_svnThread->_busy );
693 _tiedProperties.Tie( _terraRoot->getNode("active", true), (bool*) &_svnThread->_active );
694 _tiedProperties.Tie( _terraRoot->getNode("update-count", true), (int*) &_svnThread->_success_count );
695 _tiedProperties.Tie( _terraRoot->getNode("error-count", true), (int*) &_svnThread->_fail_count );
696 _tiedProperties.Tie( _terraRoot->getNode("tile-count", true), (int*) &_svnThread->_updated_tile_count );
697 _terraRoot->getNode("busy", true)->setAttribute(SGPropertyNode::WRITE,false);
698 _terraRoot->getNode("active", true)->setAttribute(SGPropertyNode::WRITE,false);
699 _terraRoot->getNode("update-count", true)->setAttribute(SGPropertyNode::WRITE,false);
700 _terraRoot->getNode("error-count", true)->setAttribute(SGPropertyNode::WRITE,false);
701 _terraRoot->getNode("tile-count", true)->setAttribute(SGPropertyNode::WRITE,false);
702 _terraRoot->getNode("use-built-in-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
703 _terraRoot->getNode("use-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
704 // stalled is used as a signal handler (to connect listeners triggering GUI pop-ups)
705 _stalled_node = _terraRoot->getNode("stalled", true);
706 _stalled_node->setBoolValue(_svnThread->_stalled);
707 _stalled_node->setAttribute(SGPropertyNode::PRESERVE,true);
710 void SGTerraSync::unbind()
713 _tiedProperties.Untie();
716 void SGTerraSync::update(double)
718 static SGBucket bucket;
719 if (_svnThread->isDirty())
721 if (!_svnThread->_active)
723 if (_svnThread->_stalled)
725 SG_LOG(SG_TERRAIN,SG_ALERT,
726 "Automatic scenery download/synchronization stalled. Too many errors.");
730 // not really an alert - just always show this message
731 SG_LOG(SG_TERRAIN,SG_ALERT,
732 "Automatic scenery download/synchronization has stopped.");
734 _stalled_node->setBoolValue(_svnThread->_stalled);
737 if (!_refresh_display->getBoolValue())
740 while (_svnThread->hasNewTiles())
742 WaitingTile next = _svnThread->getNewTile();
743 if (next._refreshScenery)
745 refreshScenery(_svnThread->getLocalDir(),next._dir);
751 void SGTerraSync::refreshScenery(SGPath path,const string& relativeDir)
753 // find tiles to be refreshed
756 path.append(relativeDir);
759 simgear::Dir dir(path);
760 //TODO need to be smarter here. only update tiles which actually
761 // changed recently. May also be possible to use information from the
762 // built-in SVN client directly (instead of checking directory contents).
763 PathList tileList = dir.children(simgear::Dir::TYPE_FILE, ".stg");
764 for (unsigned int i=0; i<tileList.size(); ++i)
766 // reload scenery tile
767 long index = atoi(tileList[i].file().c_str());
768 _tile_cache->refresh_tile(index);
774 bool SGTerraSync::isIdle() {return _svnThread->isIdle();}
776 void SGTerraSync::setTileCache(TileCache* tile_cache)
778 _tile_cache = tile_cache;
781 void SGTerraSync::syncAirportsModels()
784 for ( synced_other = 'K'; synced_other <= 'Z'; synced_other++ )
787 snprintf( dir, 512, "Airports/%c", synced_other );
788 WaitingTile w(dir,false);
789 _svnThread->request( w );
791 for ( synced_other = 'A'; synced_other <= 'J'; synced_other++ )
794 snprintf( dir, 512, "Airports/%c", synced_other );
795 WaitingTile w(dir,false);
796 _svnThread->request( w );
798 WaitingTile w("Models",false);
799 _svnThread->request( w );
803 void SGTerraSync::syncArea( int lat, int lon )
805 if ( lat < -90 || lat > 90 || lon < -180 || lon > 180 )
808 int baselat, baselon;
811 int base = (int)(lat / 10);
812 if ( lat == base * 10 ) {
815 baselat = (base - 1) * 10;
819 baselat = (int)(lat / 10) * 10;
823 int base = (int)(lon / 10);
824 if ( lon == base * 10 ) {
827 baselon = (base - 1) * 10;
831 baselon = (int)(lon / 10) * 10;
835 const char* terrainobjects[3] = { "Terrain", "Objects", 0 };
837 for (const char** tree = &terrainobjects[0]; *tree; tree++)
840 snprintf( dir, 512, "%s/%c%03d%c%02d/%c%03d%c%02d",
842 EW, abs(baselon), NS, abs(baselat),
843 EW, abs(lon), NS, abs(lat) );
844 WaitingTile w(dir,refresh);
845 _svnThread->request( w );
851 void SGTerraSync::syncAreas( int lat, int lon, int lat_dir, int lon_dir )
853 if ( lat_dir == 0 && lon_dir == 0 ) {
854 // do surrounding 8 1x1 degree areas.
855 for ( int i = lat - 1; i <= lat + 1; ++i ) {
856 for ( int j = lon - 1; j <= lon + 1; ++j ) {
857 if ( i != lat || j != lon ) {
863 if ( lat_dir != 0 ) {
864 syncArea( lat + lat_dir, lon - 1 );
865 syncArea( lat + lat_dir, lon + 1 );
866 syncArea( lat + lat_dir, lon );
868 if ( lon_dir != 0 ) {
869 syncArea( lat - 1, lon + lon_dir );
870 syncArea( lat + 1, lon + lon_dir );
871 syncArea( lat, lon + lon_dir );
875 // do current 1x1 degree area first
876 syncArea( lat, lon );
880 bool SGTerraSync::schedulePosition(int lat, int lon)
882 // Ignore messages where the location does not change
883 if ( lat != last_lat || lon != last_lon )
885 SG_LOG(SG_TERRAIN,SG_DEBUG, "Requesting scenery update for position " <<
887 int lat_dir, lon_dir, dist;
888 if ( last_lat == NOWHERE || last_lon == NOWHERE )
890 lat_dir = lon_dir = 0;
893 dist = lat - last_lat;
896 lat_dir = dist / abs(dist);
902 dist = lon - last_lon;
905 lon_dir = dist / abs(dist);
912 SG_LOG(SG_TERRAIN,SG_DEBUG, "Scenery update for " <<
913 "lat = " << lat << ", lon = " << lon <<
914 ", lat_dir = " << lat_dir << ", " <<
915 "lon_dir = " << lon_dir);
917 syncAreas( lat, lon, lat_dir, lon_dir );