//
// $Id$
-#ifdef HAVE_CONFIG_H
-# include <simgear_config.h>
-#endif
-
+#include <simgear_config.h>
#include <simgear/compiler.h>
#ifdef HAVE_WINDOWS_H
#include <string>
#include <map>
-#include <simgear/compiler.h>
+#include <simgear/version.h>
#include "terrasync.hxx"
#include <simgear/props/props_io.hxx>
#include <simgear/io/HTTPClient.hxx>
#include <simgear/io/SVNRepository.hxx>
-
+#include <simgear/io/HTTPRepository.hxx>
+#include <simgear/io/DNSClient.hxx>
+#include <simgear/structure/exception.hxx>
+#include <simgear/math/sg_random.h>
static const bool svn_built_in_available = true;
using namespace simgear;
using namespace std;
-const char* rsync_cmd =
+const char* rsync_cmd =
"rsync --verbose --archive --delete --perms --owner --group";
const char* svn_options =
static const double FailedAttempt = 10*60;
}
-typedef map<string,time_t> CompletedTiles;
+typedef map<string,time_t> TileAgeCache;
///////////////////////////////////////////////////////////////////////////////
// helper functions ///////////////////////////////////////////////////////////
}
///////////////////////////////////////////////////////////////////////////////
-// WaitingTile ////////////////////////////////////////////////////////////////
+// SyncItem ////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
-class WaitingTile
+class SyncItem
{
public:
- WaitingTile(string dir,bool refresh) :
- _dir(dir), _refreshScenery(refresh) {}
+ enum Type
+ {
+ Stop = 0, ///< special item indicating to stop the SVNThread
+ Tile,
+ AirportData,
+ SharedModels,
+ AIData
+ };
+
+ enum Status
+ {
+ Invalid = 0,
+ Waiting,
+ Cached, ///< using already cached result
+ Updated,
+ NotFound,
+ Failed
+ };
+
+ SyncItem() :
+ _dir(),
+ _type(Stop),
+ _status(Invalid)
+ {
+ }
+
+ SyncItem(string dir, Type ty) :
+ _dir(dir),
+ _type(ty),
+ _status(Waiting)
+ {}
+
string _dir;
- bool _refreshScenery;
+ Type _type;
+ Status _status;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+/**
+ * @brief SyncSlot encapsulates a queue of sync items we will fetch
+ * serially. Multiple slots exist to sync different types of item in
+ * parallel.
+ */
+class SyncSlot
+{
+public:
+ SyncSlot() :
+ isNewDirectory(false),
+ busy(false)
+ {}
+
+ SyncItem currentItem;
+ bool isNewDirectory;
+ std::queue<SyncItem> queue;
+ std::auto_ptr<AbstractRepository> repository;
+ SGTimeStamp stamp;
+ bool busy; ///< is the slot working or idle
+
+ unsigned int nextWarnTimeout;
+};
+
+static const int SYNC_SLOT_TILES = 0; ///< Terrain and Objects sync
+static const int SYNC_SLOT_SHARED_DATA = 1; /// shared Models and Airport data
+static const int SYNC_SLOT_AI_DATA = 2; /// AI traffic and models
+static const unsigned int NUM_SYNC_SLOTS = 3;
+
+/**
+ * @brief translate a sync item type into one of the available slots.
+ * This provides the scheduling / balancing / prioritising between slots.
+ */
+static unsigned int syncSlotForType(SyncItem::Type ty)
+{
+ switch (ty) {
+ case SyncItem::Tile: return SYNC_SLOT_TILES;
+ case SyncItem::SharedModels:
+ case SyncItem::AirportData:
+ return SYNC_SLOT_SHARED_DATA;
+ case SyncItem::AIData:
+ return SYNC_SLOT_AI_DATA;
+
+ default:
+ return SYNC_SLOT_SHARED_DATA;
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Base server query
+///////////////////////////////////////////////////////////////////////////////
+
+class ServerSelectQuery : public HTTP::Request
+{
+public:
+ ServerSelectQuery() :
+ HTTP::Request("http://scenery.flightgear.org/svn-server", "GET")
+ {
+ }
+
+ std::string svnUrl() const
+ {
+ std::string s = simgear::strutils::strip(m_url);
+ if (s.at(s.length() - 1) == '/') {
+ s = s.substr(0, s.length() - 1);
+ }
+
+ return s;
+ }
+protected:
+ virtual void gotBodyData(const char* s, int n)
+ {
+ m_url.append(std::string(s, n));
+ }
+
+ virtual void onFail()
+ {
+ SG_LOG(SG_TERRASYNC, SG_ALERT, "Failed to query TerraSync SVN server");
+ HTTP::Request::onFail();
+ }
+
+private:
+ std::string m_url;
};
+
///////////////////////////////////////////////////////////////////////////////
// SGTerraSync::SvnThread /////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
void stop();
bool start();
- bool isIdle() {return waitingTiles.empty();}
- void request(const WaitingTile& dir) {waitingTiles.push_front(dir);}
+ bool isIdle() {return !_busy; }
+ void request(const SyncItem& dir) {waitingTiles.push_front(dir);}
bool isDirty() { bool r = _is_dirty;_is_dirty = false;return r;}
bool hasNewTiles() { return !_freshTiles.empty();}
- WaitingTile getNewTile() { return _freshTiles.pop_front();}
+ SyncItem getNewTile() { return _freshTiles.pop_front();}
void setSvnServer(string server) { _svn_server = stripPath(server);}
+ void setSvnDataServer(string server) { _svn_data_server = stripPath(server);}
+ void setHTTPServer(const std::string& server)
+ {
+ _httpServer = stripPath(server);
+ }
+
void setExtSvnUtility(string svn_util) { _svn_command = simgear::strutils::strip(svn_util);}
void setRsyncServer(string server) { _rsync_server = simgear::strutils::strip(server);}
void setLocalDir(string dir) { _local_dir = stripPath(dir);}
volatile int _consecutive_errors;
volatile int _allowed_errors;
volatile int _cache_hits;
+ volatile int _transfer_rate;
+ // kbytes, not bytes, because bytes might overflow 2^31
+ volatile int _total_kb_downloaded;
+
private:
virtual void run();
- bool syncTree(const char* dir, bool& isNewDirectory);
- bool syncTreeExternal(const char* dir);
-
- bool isPathCached(const WaitingTile& next) const;
- void syncPath(const WaitingTile& next);
-
- void initCompletedTilesPersistentCache();
- void writeCompletedTilesPersistentCache() const;
-
- bool syncTreeInternal(const char* dir);
-
- bool _use_built_in;
- HTTP::Client _http;
- std::auto_ptr<SVNRepository> _repository;
+ // external model run and helpers
+ void runExternal();
+ void syncPathExternal(const SyncItem& next);
+ bool runExternalSyncCommand(const char* dir);
+
+ // internal mode run and helpers
+ void runInternal();
+ void updateSyncSlot(SyncSlot& slot);
+
+ // commond helpers between both internal and external models
+
+ SyncItem::Status isPathCached(const SyncItem& next) const;
+ void initCompletedTilesPersistentCache();
+ void writeCompletedTilesPersistentCache() const;
+ void updated(SyncItem item, bool isNewDirectory);
+ void fail(SyncItem failedItem);
+ void notFound(SyncItem notFoundItem);
+
+ bool _use_built_in;
+ HTTP::Client _http;
+ SyncSlot _syncSlots[NUM_SYNC_SLOTS];
volatile bool _is_dirty;
volatile bool _stop;
- SGBlockingDeque <WaitingTile> waitingTiles;
- CompletedTiles _completedTiles;
- SGBlockingDeque <WaitingTile> _freshTiles;
+ SGBlockingDeque <SyncItem> waitingTiles;
+
+ TileAgeCache _completedTiles;
+ TileAgeCache _notFoundItems;
+
+ SGBlockingDeque <SyncItem> _freshTiles;
bool _use_svn;
string _svn_server;
+ string _svn_data_server;
string _svn_command;
string _rsync_server;
string _local_dir;
SGPath _persistentCachePath;
+ string _httpServer;
};
SGTerraSync::SvnThread::SvnThread() :
_consecutive_errors(0),
_allowed_errors(6),
_cache_hits(0),
+ _transfer_rate(0),
+ _total_kb_downloaded(0),
_use_built_in(true),
_is_dirty(false),
_stop(false),
_use_svn(true)
{
+ _http.setUserAgent("terrascenery-" SG_STRINGIZE(SIMGEAR_VERSION));
}
void SGTerraSync::SvnThread::stop()
// set stop flag and wake up the thread with an empty request
_stop = true;
- WaitingTile w("",false);
+ SyncItem w(string(), SyncItem::Stop);
request(w);
join();
_running = false;
if (_local_dir=="")
{
- SG_LOG(SG_TERRAIN,SG_ALERT,
+ SG_LOG(SG_TERRASYNC,SG_ALERT,
"Cannot start scenery download. Local cache directory is undefined.");
_fail_count++;
_stalled = true;
SGPath path(_local_dir);
if (!path.exists())
{
- SG_LOG(SG_TERRAIN,SG_ALERT,
+ SG_LOG(SG_TERRASYNC,SG_ALERT,
"Cannot start scenery download. Directory '" << _local_dir <<
"' does not exist. Set correct directory path or create directory folder.");
_fail_count++;
path.append("version");
if (path.exists())
{
- SG_LOG(SG_TERRAIN,SG_ALERT,
+ SG_LOG(SG_TERRASYNC,SG_ALERT,
"Cannot start scenery download. Directory '" << _local_dir <<
"' contains the base package. Use a separate directory.");
_fail_count++;
_use_svn |= _use_built_in;
- if ((_use_svn)&&(_svn_server==""))
- {
- SG_LOG(SG_TERRAIN,SG_ALERT,
- "Cannot start scenery download. Subversion scenery server is undefined.");
- _fail_count++;
- _stalled = true;
- return false;
- }
+
if ((!_use_svn)&&(_rsync_server==""))
{
- SG_LOG(SG_TERRAIN,SG_ALERT,
+ SG_LOG(SG_TERRASYNC,SG_ALERT,
"Cannot start scenery download. Rsync scenery server is undefined.");
_fail_count++;
_stalled = true;
// not really an alert - but we want to (always) see this message, so user is
// aware we're downloading scenery (and using bandwidth).
- SG_LOG(SG_TERRAIN,SG_ALERT,
+ SG_LOG(SG_TERRASYNC,SG_ALERT,
"Starting automatic scenery download/synchronization. "
<< status
<< "Directory: '" << _local_dir << "'.");
return true;
}
-// sync one directory tree
-bool SGTerraSync::SvnThread::syncTree(const char* dir, bool& isNewDirectory)
-{
- int rc;
- SGPath path( _local_dir );
-
- path.append( dir );
- isNewDirectory = !path.exists();
- if (isNewDirectory)
- {
- rc = path.create_dir( 0755 );
- if (rc)
- {
- SG_LOG(SG_TERRAIN,SG_ALERT,
- "Cannot create directory '" << dir << "', return code = " << rc );
- return false;
- }
- }
-
- if (_use_built_in)
- return syncTreeInternal(dir);
- else
- return syncTreeExternal(dir);
-}
-
-bool SGTerraSync::SvnThread::syncTreeInternal(const char* dir)
-{
- ostringstream command;
- command << _svn_server << "/" << dir;
-
- SGPath path(_local_dir);
- path.append(dir);
- _repository.reset(new SVNRepository(path, &_http));
- _repository->setBaseUrl(command.str());
-
- SGTimeStamp st;
- st.stamp();
- SG_LOG(SG_IO, SG_DEBUG, "terrasync: will sync " << command.str());
- _repository->update();
-
- bool result = true;
- while (!_stop && _repository->isDoingSync()) {
- _http.update(100);
- }
-
- if (_repository->failure() == SVNRepository::SVN_ERROR_NOT_FOUND) {
- // this is fine, but maybe we should use a different return code
- // in the future to higher layers can distuinguish this case
- } else if (_repository->failure() != SVNRepository::SVN_NO_ERROR) {
- result = false;
- } else {
- SG_LOG(SG_IO, SG_DEBUG, "sync of " << command.str() << " finished ("
- << st.elapsedMSec() << " msec");
- }
-
- _repository.reset();
- return result;
-}
-
-bool SGTerraSync::SvnThread::syncTreeExternal(const char* dir)
+bool SGTerraSync::SvnThread::runExternalSyncCommand(const char* dir)
{
ostringstream buf;
SGPath localPath( _local_dir );
// windows command line parsing is just lovely...
// to allow white spaces, the system call needs this:
// ""C:\Program Files\something.exe" somearg "some other arg""
- // Note: whitespace strings quoted by a pair of "" _and_ the
+ // Note: whitespace strings quoted by a pair of "" _and_ the
// entire string needs to be wrapped by "" too.
// The svn url needs forward slashes (/) as a path separator while
// the local path needs windows-native backslash as a path separator.
#else
command = buf.str();
#endif
- SG_LOG(SG_TERRAIN,SG_DEBUG, "sync command '" << command << "'");
+ SG_LOG(SG_TERRASYNC,SG_DEBUG, "sync command '" << command << "'");
#ifdef SG_WINDOWS
// tbd: does Windows support "popen"?
if (rc)
{
- SG_LOG(SG_TERRAIN,SG_ALERT,
+ SG_LOG(SG_TERRASYNC,SG_ALERT,
"Failed to synchronize directory '" << dir << "', " <<
"error code= " << rc);
return false;
void SGTerraSync::SvnThread::run()
{
_active = true;
-
initCompletedTilesPersistentCache();
-
+
+ if (_httpServer == "automatic" ) {
+
+ //TODO: make DN and service settable from properties
+ DNS::NAPTRRequest * naptrRequest = new DNS::NAPTRRequest("terrasync.flightgear.org");
+ naptrRequest->qservice = "ws20";
+
+ naptrRequest->qflags = "U";
+ DNS::Request_ptr r(naptrRequest);
+
+ DNS::Client dnsClient;
+ dnsClient.makeRequest(r);
+ while( !r->isComplete() && !r->isTimeout() )
+ dnsClient.update(0);
+
+ if( naptrRequest->entries.empty() ) {
+ SG_LOG(SG_TERRASYNC, SG_ALERT, "ERROR: automatic terrasync http-server requested, but no DNS entry found.");
+ _httpServer = "";
+ } else {
+ // walk through responses, they are ordered by 1. order and 2. preference
+ // For now, only take entries with lowest order
+ // TODO: try all available servers in the order given by preferenc and order
+ int order = naptrRequest->entries[0]->order;
+
+ // get all servers with this order and the same (for now only lowest preference)
+ DNS::NAPTRRequest::NAPTR_list availableServers;
+ for( DNS::NAPTRRequest::NAPTR_list::const_iterator it = naptrRequest->entries.begin();
+ it != naptrRequest->entries.end();
+ ++it ) {
+
+ if( (*it)->order != order )
+ continue;
+
+ string regex = (*it)->regexp;
+ if( false == simgear::strutils::starts_with( (*it)->regexp, "!^.*$!" ) ) {
+ SG_LOG(SG_TERRASYNC,SG_WARN, "ignoring unsupported regexp: " << (*it)->regexp );
+ continue;
+ }
+
+ if( false == simgear::strutils::ends_with( (*it)->regexp, "!" ) ) {
+ SG_LOG(SG_TERRASYNC,SG_WARN, "ignoring unsupported regexp: " << (*it)->regexp );
+ continue;
+ }
+
+ // always use first entry
+ if( availableServers.empty() || (*it)->preference == availableServers[0]->preference) {
+ SG_LOG(SG_TERRASYNC,SG_DEBUG, "available server regexp: " << (*it)->regexp );
+ availableServers.push_back( *it );
+ }
+ }
+
+ // now pick a random entry from the available servers
+ DNS::NAPTRRequest::NAPTR_list::size_type idx = sg_random() * availableServers.size();
+ _httpServer = availableServers[idx]->regexp;
+ _httpServer = _httpServer.substr( 6, _httpServer.length()-7 ); // strip search pattern and separators
+
+ SG_LOG(SG_TERRASYNC,SG_INFO, "picking entry # " << idx << ", server is " << _httpServer );
+ }
+ }
+ if( _httpServer.empty() ) { // don't resolve SVN server is HTTP server is set
+ if ( _svn_server.empty()) {
+ SG_LOG(SG_TERRASYNC,SG_INFO, "Querying closest TerraSync server");
+ ServerSelectQuery* ssq = new ServerSelectQuery;
+ HTTP::Request_ptr req = ssq;
+ _http.makeRequest(req);
+ while (!req->isComplete()) {
+ _http.update(20);
+ }
+
+ if (req->readyState() == HTTP::Request::DONE) {
+ _svn_server = ssq->svnUrl();
+ SG_LOG(SG_TERRASYNC,SG_INFO, "Closest TerraSync server:" << _svn_server);
+ } else {
+ SG_LOG(SG_TERRASYNC,SG_WARN, "Failed to query closest TerraSync server");
+ }
+ } else {
+ SG_LOG(SG_TERRASYNC,SG_INFO, "Explicit: TerraSync server:" << _svn_server);
+ }
+
+ if (_svn_server.empty()) {
+#if WE_HAVE_A_DEFAULT_SERVER_BUT_WE_DONT_THIS_URL_IS_NO_LONGER_VALID
+ // default value
+ _svn_server = "http://foxtrot.mgras.net:8080/terrascenery/trunk/data/Scenery";
+#endif
+ }
+ }
+
+ if (_use_built_in) {
+ runInternal();
+ } else {
+ runExternal();
+ }
+
+ _active = false;
+ _running = false;
+ _is_dirty = true;
+}
+
+void SGTerraSync::SvnThread::runExternal()
+{
while (!_stop)
{
- WaitingTile next = waitingTiles.pop_front();
+ SyncItem next = waitingTiles.pop_front();
if (_stop)
break;
- if (isPathCached(next)) {
+ SyncItem::Status cacheStatus = isPathCached(next);
+ if (cacheStatus != SyncItem::Invalid) {
_cache_hits++;
- SG_LOG(SG_TERRAIN, SG_DEBUG,
+ SG_LOG(SG_TERRASYNC, SG_DEBUG,
"Cache hit for: '" << next._dir << "'");
+ next._status = cacheStatus;
+ _freshTiles.push_back(next);
+ _is_dirty = true;
continue;
}
-
- syncPath(next);
+
+ syncPathExternal(next);
if ((_allowed_errors >= 0)&&
(_consecutive_errors >= _allowed_errors))
_stalled = true;
_stop = true;
}
+ } // of thread running loop
+}
+
+void SGTerraSync::SvnThread::syncPathExternal(const SyncItem& next)
+{
+ _busy = true;
+ SGPath path( _local_dir );
+ path.append( next._dir );
+ bool isNewDirectory = !path.exists();
+
+ try {
+ if (isNewDirectory) {
+ int rc = path.create_dir( 0755 );
+ if (rc) {
+ SG_LOG(SG_TERRASYNC,SG_ALERT,
+ "Cannot create directory '" << path << "', return code = " << rc );
+ throw sg_exception("Cannot create directory for terrasync", path.str());
+ }
+ }
+
+ if (!runExternalSyncCommand(next._dir.c_str())) {
+ throw sg_exception("Running external sync command failed");
+ }
+ } catch (sg_exception& e) {
+ fail(next);
+ _busy = false;
+ return;
}
- _active = false;
- _running = false;
- _is_dirty = true;
+ updated(next, isNewDirectory);
+ _busy = false;
+}
+
+void SGTerraSync::SvnThread::updateSyncSlot(SyncSlot &slot)
+{
+ if (slot.repository.get()) {
+ if (slot.repository->isDoingSync()) {
+#if 1
+ if (slot.stamp.elapsedMSec() > (int)slot.nextWarnTimeout) {
+ SG_LOG(SG_TERRASYNC, SG_INFO, "sync taking a long time:" << slot.currentItem._dir << " taken " << slot.stamp.elapsedMSec());
+ SG_LOG(SG_TERRASYNC, SG_INFO, "HTTP request count:" << _http.hasActiveRequests());
+ slot.nextWarnTimeout += 10000;
+ }
+#endif
+ return; // easy, still working
+ }
+
+ // check result
+ SVNRepository::ResultCode res = slot.repository->failure();
+ if (res == AbstractRepository::REPO_ERROR_NOT_FOUND) {
+ notFound(slot.currentItem);
+ } else if (res != AbstractRepository::REPO_NO_ERROR) {
+ fail(slot.currentItem);
+ } else {
+ updated(slot.currentItem, slot.isNewDirectory);
+ SG_LOG(SG_TERRASYNC, SG_DEBUG, "sync of " << slot.repository->baseUrl() << " finished ("
+ << slot.stamp.elapsedMSec() << " msec");
+ }
+
+ // whatever happened, we're done with this repository instance
+ slot.busy = false;
+ slot.repository.reset();
+ }
+
+ // init and start sync of the next repository
+ if (!slot.queue.empty()) {
+ slot.currentItem = slot.queue.front();
+ slot.queue.pop();
+
+ SGPath path(_local_dir);
+ path.append(slot.currentItem._dir);
+ slot.isNewDirectory = !path.exists();
+ if (slot.isNewDirectory) {
+ int rc = path.create_dir( 0755 );
+ if (rc) {
+ SG_LOG(SG_TERRASYNC,SG_ALERT,
+ "Cannot create directory '" << path << "', return code = " << rc );
+ fail(slot.currentItem);
+ return;
+ }
+ } // of creating directory step
+
+ string serverUrl(_svn_server);
+ if (!_httpServer.empty()) {
+ slot.repository.reset(new HTTPRepository(path, &_http));
+ serverUrl = _httpServer;
+ } else {
+ if (slot.currentItem._type == SyncItem::AIData) {
+ serverUrl = _svn_data_server;
+ }
+ slot.repository.reset(new SVNRepository(path, &_http));
+ }
+
+ slot.repository->setBaseUrl(serverUrl + "/" + slot.currentItem._dir);
+ try {
+ slot.repository->update();
+ } catch (sg_exception& e) {
+ SG_LOG(SG_TERRASYNC, SG_INFO, "sync of " << slot.repository->baseUrl() << " failed to start with error:"
+ << e.getFormattedMessage());
+ fail(slot.currentItem);
+ slot.busy = false;
+ slot.repository.reset();
+ return;
+ }
+
+ slot.nextWarnTimeout = 20000;
+ slot.stamp.stamp();
+ slot.busy = true;
+ SG_LOG(SG_TERRASYNC, SG_INFO, "sync of " << slot.repository->baseUrl() << " started, queue size is " << slot.queue.size());
+ }
}
-bool SGTerraSync::SvnThread::isPathCached(const WaitingTile& next) const
+void SGTerraSync::SvnThread::runInternal()
{
- CompletedTiles::const_iterator ii = _completedTiles.find( next._dir );
+ while (!_stop) {
+ try {
+ _http.update(100);
+ } catch (sg_exception& e) {
+ SG_LOG(SG_TERRASYNC, SG_WARN, "failure doing HTTP update" << e.getFormattedMessage());
+ }
+
+ _transfer_rate = _http.transferRateBytesPerSec();
+ // convert from bytes to kbytes
+ _total_kb_downloaded = static_cast<int>(_http.totalBytesDownloaded() / 1024);
+
+ if (_stop)
+ break;
+
+ // drain the waiting tiles queue into the sync slot queues.
+ while (!waitingTiles.empty()) {
+ SyncItem next = waitingTiles.pop_front();
+ SyncItem::Status cacheStatus = isPathCached(next);
+ if (cacheStatus != SyncItem::Invalid) {
+ _cache_hits++;
+ SG_LOG(SG_TERRASYNC, SG_DEBUG, "\nTerraSync Cache hit for: '" << next._dir << "'");
+ next._status = cacheStatus;
+ _freshTiles.push_back(next);
+ _is_dirty = true;
+ continue;
+ }
+
+ unsigned int slot = syncSlotForType(next._type);
+ _syncSlots[slot].queue.push(next);
+ }
+
+ bool anySlotBusy = false;
+ // update each sync slot in turn
+ for (unsigned int slot=0; slot < NUM_SYNC_SLOTS; ++slot) {
+ updateSyncSlot(_syncSlots[slot]);
+ anySlotBusy |= _syncSlots[slot].busy;
+ }
+
+ _busy = anySlotBusy;
+ if (!anySlotBusy) {
+ // wait on the blocking deque here, otherwise we spin
+ // the loop very fast, since _http::update with no connections
+ // active returns immediately.
+ waitingTiles.waitOnNotEmpty();
+ }
+ } // of thread running loop
+}
+
+SyncItem::Status SGTerraSync::SvnThread::isPathCached(const SyncItem& next) const
+{
+ TileAgeCache::const_iterator ii = _completedTiles.find( next._dir );
if (ii == _completedTiles.end()) {
- return false;
+ ii = _notFoundItems.find( next._dir );
+ // Invalid means 'not cached', otherwise we want to return to
+ // higher levels the cache status
+ return (ii == _notFoundItems.end()) ? SyncItem::Invalid : SyncItem::NotFound;
}
-
- // check if the path still physically exists
+
+ // check if the path still physically exists. This is needed to
+ // cope with the user manipulating our cache dir
SGPath p(_local_dir);
p.append(next._dir);
if (!p.exists()) {
- return false;
+ return SyncItem::Invalid;
}
-
+
time_t now = time(0);
- return (ii->second > now);
+ return (ii->second > now) ? SyncItem::Cached : SyncItem::Invalid;
}
-void SGTerraSync::SvnThread::syncPath(const WaitingTile& next)
+void SGTerraSync::SvnThread::fail(SyncItem failedItem)
{
- bool isNewDirectory = false;
time_t now = time(0);
-
- _busy = true;
- if (!syncTree(next._dir.c_str(),isNewDirectory))
- {
- _consecutive_errors++;
- _fail_count++;
- _completedTiles[ next._dir ] = now + UpdateInterval::FailedAttempt;
- }
- else
- {
- _consecutive_errors = 0;
- _success_count++;
- SG_LOG(SG_TERRAIN,SG_INFO,
- "Successfully synchronized directory '" << next._dir << "'");
- if (next._refreshScenery)
- {
- // updated a tile
- _updated_tile_count++;
- if (isNewDirectory)
- {
- // for now only report new directories to refresh display
- // (i.e. only when ocean needs to be replaced with actual data)
- _freshTiles.push_back(next);
- _is_dirty = true;
- }
- }
-
- _completedTiles[ next._dir ] = now + UpdateInterval::SuccessfulAttempt;
- writeCompletedTilesPersistentCache();
+ _consecutive_errors++;
+ _fail_count++;
+ failedItem._status = SyncItem::Failed;
+ _freshTiles.push_back(failedItem);
+ SG_LOG(SG_TERRASYNC,SG_INFO,
+ "Failed to sync'" << failedItem._dir << "'");
+ _completedTiles[ failedItem._dir ] = now + UpdateInterval::FailedAttempt;
+ _is_dirty = true;
+}
+
+void SGTerraSync::SvnThread::notFound(SyncItem item)
+{
+ // treat not found as authorative, so use the same cache expiry
+ // as succesful download. Important for MP models and similar so
+ // we don't spam the server with lookups for models that don't
+ // exist
+
+ time_t now = time(0);
+ item._status = SyncItem::NotFound;
+ _freshTiles.push_back(item);
+ _is_dirty = true;
+ _notFoundItems[ item._dir ] = now + UpdateInterval::SuccessfulAttempt;
+ writeCompletedTilesPersistentCache();
+}
+
+void SGTerraSync::SvnThread::updated(SyncItem item, bool isNewDirectory)
+{
+ time_t now = time(0);
+ _consecutive_errors = 0;
+ _success_count++;
+ SG_LOG(SG_TERRASYNC,SG_INFO,
+ "Successfully synchronized directory '" << item._dir << "'");
+
+ item._status = SyncItem::Updated;
+ if (item._type == SyncItem::Tile) {
+ _updated_tile_count++;
}
- _busy = false;
+
+ _freshTiles.push_back(item);
+ _completedTiles[ item._dir ] = now + UpdateInterval::SuccessfulAttempt;
+ _is_dirty = true;
+ writeCompletedTilesPersistentCache();
}
void SGTerraSync::SvnThread::initCompletedTilesPersistentCache()
if (!_persistentCachePath.exists()) {
return;
}
-
+
SGPropertyNode_ptr cacheRoot(new SGPropertyNode);
time_t now = time(0);
-
- readProperties(_persistentCachePath.str(), cacheRoot);
+
+ try {
+ readProperties(_persistentCachePath.str(), cacheRoot);
+ } catch (sg_exception& e) {
+ SG_LOG(SG_TERRASYNC, SG_INFO, "corrupted persistent cache, discarding");
+ return;
+ }
+
for (int i=0; i<cacheRoot->nChildren(); ++i) {
SGPropertyNode* entry = cacheRoot->getChild(i);
+ bool isNotFound = (strcmp(entry->getName(), "not-found") == 0);
string tileName = entry->getStringValue("path");
time_t stamp = entry->getIntValue("stamp");
if (stamp < now) {
continue;
}
-
- _completedTiles[tileName] = stamp;
+
+ if (isNotFound) {
+ _completedTiles[tileName] = stamp;
+ } else {
+ _notFoundItems[tileName] = stamp;
+ }
}
}
if (_persistentCachePath.isNull()) {
return;
}
-
+
std::ofstream f(_persistentCachePath.c_str(), std::ios::trunc);
if (!f.is_open()) {
return;
}
-
+
SGPropertyNode_ptr cacheRoot(new SGPropertyNode);
- CompletedTiles::const_iterator it = _completedTiles.begin();
+ TileAgeCache::const_iterator it = _completedTiles.begin();
for (; it != _completedTiles.end(); ++it) {
SGPropertyNode* entry = cacheRoot->addChild("entry");
entry->setStringValue("path", it->first);
entry->setIntValue("stamp", it->second);
}
-
+
+ it = _notFoundItems.begin();
+ for (; it != _notFoundItems.end(); ++it) {
+ SGPropertyNode* entry = cacheRoot->addChild("not-found");
+ entry->setStringValue("path", it->first);
+ entry->setIntValue("stamp", it->second);
+ }
+
writeProperties(f, cacheRoot, true /* write_all */);
f.close();
}
///////////////////////////////////////////////////////////////////////////////
// SGTerraSync ////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
-SGTerraSync::SGTerraSync(SGPropertyNode_ptr root) :
+SGTerraSync::SGTerraSync() :
_svnThread(NULL),
- last_lat(NOWHERE),
- last_lon(NOWHERE),
- _terraRoot(root->getNode("/sim/terrasync",true)),
- _refreshCb(NULL),
- _userCbData(NULL)
+ _bound(false),
+ _inited(false)
{
_svnThread = new SvnThread();
- _log = new BufferedLogCallback(SG_TERRAIN, SG_INFO);
+ _log = new BufferedLogCallback(SG_TERRASYNC, SG_INFO);
_log->truncateAt(255);
-
+
sglog().addCallback(_log);
}
SGTerraSync::~SGTerraSync()
{
- _tiedProperties.Untie();
delete _svnThread;
_svnThread = NULL;
sglog().removeCallback(_log);
delete _log;
+ _tiedProperties.Untie();
+}
+
+void SGTerraSync::setRoot(SGPropertyNode_ptr root)
+{
+ _terraRoot = root->getNode("/sim/terrasync",true);
}
void SGTerraSync::init()
{
- _refreshDisplay = _terraRoot->getNode("refresh-display",true);
+ if (_inited) {
+ return;
+ }
+
+ _inited = true;
+
+ assert(_terraRoot);
_terraRoot->setBoolValue("built-in-svn-available",svn_built_in_available);
+
reinit();
}
+void SGTerraSync::shutdown()
+{
+ _svnThread->stop();
+}
+
void SGTerraSync::reinit()
{
// do not reinit when enabled and we're already up and running
if ((_terraRoot->getBoolValue("enabled",false))&&
(_svnThread->_active && _svnThread->_running))
+ {
return;
+ }
_svnThread->stop();
if (_terraRoot->getBoolValue("enabled",false))
{
_svnThread->setSvnServer(_terraRoot->getStringValue("svn-server",""));
+ std::string httpServer(_terraRoot->getStringValue("http-server",""));
+ _svnThread->setHTTPServer(httpServer);
+ _svnThread->setSvnDataServer(_terraRoot->getStringValue("svn-data-server",""));
_svnThread->setRsyncServer(_terraRoot->getStringValue("rsync-server",""));
_svnThread->setLocalDir(_terraRoot->getStringValue("scenery-dir",""));
_svnThread->setAllowedErrorCount(_terraRoot->getIntValue("max-errors",5));
_svnThread->setUseBuiltin(_terraRoot->getBoolValue("use-built-in-svn",true));
- _svnThread->setCachePath(SGPath(_terraRoot->getStringValue("cache-path","")));
+
+ if (httpServer.empty()) {
+ // HTTP doesn't benefit from using the persistent cache
+ _svnThread->setCachePath(SGPath(_terraRoot->getStringValue("cache-path","")));
+ } else {
+ SG_LOG(SG_TERRASYNC, SG_INFO, "HTTP repository selected, disabling persistent cache");
+ }
+
_svnThread->setCacheHits(_terraRoot->getIntValue("cache-hit", 0));
_svnThread->setUseSvn(_terraRoot->getBoolValue("use-svn",true));
_svnThread->setExtSvnUtility(_terraRoot->getStringValue("ext-svn-utility","svn"));
if (_svnThread->start())
{
syncAirportsModels();
- if (last_lat != NOWHERE && last_lon != NOWHERE)
- {
- // reschedule most recent position
- int lat = last_lat;
- int lon = last_lon;
- last_lat = NOWHERE;
- last_lon = NOWHERE;
- schedulePosition(lat, lon);
- }
}
}
void SGTerraSync::bind()
{
+ if (_bound) {
+ return;
+ }
+
+ _bound = true;
_tiedProperties.Tie( _terraRoot->getNode("busy", true), (bool*) &_svnThread->_busy );
_tiedProperties.Tie( _terraRoot->getNode("active", true), (bool*) &_svnThread->_active );
_tiedProperties.Tie( _terraRoot->getNode("update-count", true), (int*) &_svnThread->_success_count );
_tiedProperties.Tie( _terraRoot->getNode("error-count", true), (int*) &_svnThread->_fail_count );
_tiedProperties.Tie( _terraRoot->getNode("tile-count", true), (int*) &_svnThread->_updated_tile_count );
_tiedProperties.Tie( _terraRoot->getNode("cache-hits", true), (int*) &_svnThread->_cache_hits );
-
+ _tiedProperties.Tie( _terraRoot->getNode("transfer-rate-bytes-sec", true), (int*) &_svnThread->_transfer_rate );
+
+ // use kbytes here because propety doesn't support 64-bit and we might conceivably
+ // download more than 2G in a single session
+ _tiedProperties.Tie( _terraRoot->getNode("downloaded-kbytes", true), (int*) &_svnThread->_total_kb_downloaded );
+
_terraRoot->getNode("busy", true)->setAttribute(SGPropertyNode::WRITE,false);
_terraRoot->getNode("active", true)->setAttribute(SGPropertyNode::WRITE,false);
_terraRoot->getNode("update-count", true)->setAttribute(SGPropertyNode::WRITE,false);
{
_svnThread->stop();
_tiedProperties.Untie();
+ _bound = false;
+ _inited = false;
+
+ _terraRoot.clear();
+ _stalledNode.clear();
+ _cacheHits.clear();
}
void SGTerraSync::update(double)
{
if (_svnThread->_stalled)
{
- SG_LOG(SG_TERRAIN,SG_ALERT,
+ SG_LOG(SG_TERRASYNC,SG_ALERT,
"Automatic scenery download/synchronization stalled. Too many errors.");
}
else
{
// not really an alert - just always show this message
- SG_LOG(SG_TERRAIN,SG_ALERT,
+ SG_LOG(SG_TERRASYNC,SG_ALERT,
"Automatic scenery download/synchronization has stopped.");
}
_stalledNode->setBoolValue(_svnThread->_stalled);
}
- if (!_refreshDisplay->getBoolValue())
- return;
-
while (_svnThread->hasNewTiles())
{
- WaitingTile next = _svnThread->getNewTile();
- if (next._refreshScenery)
- {
- refreshScenery(_svnThread->getLocalDir(),next._dir);
- }
- }
- }
-}
+ SyncItem next = _svnThread->getNewTile();
-void SGTerraSync::refreshScenery(SGPath path,const string& relativeDir)
-{
- // find tiles to be refreshed
- if (_refreshCb)
- {
- path.append(relativeDir);
- if (path.exists())
- {
- simgear::Dir dir(path);
- //TODO need to be smarter here. only update tiles which actually
- // changed recently. May also be possible to use information from the
- // built-in SVN client directly (instead of checking directory contents).
- PathList tileList = dir.children(simgear::Dir::TYPE_FILE, ".stg");
- for (unsigned int i=0; i<tileList.size(); ++i)
- {
- // reload scenery tile
- long index = atoi(tileList[i].file().c_str());
- _refreshCb(_userCbData, index);
+ if ((next._type == SyncItem::Tile) || (next._type == SyncItem::AIData)) {
+ _activeTileDirs.erase(next._dir);
}
- }
+ } // of freshly synced items
}
}
bool SGTerraSync::isIdle() {return _svnThread->isIdle();}
-void SGTerraSync::setTileRefreshCb(SGTerraSyncCallback refreshCb, void* userCbData)
-{
- _refreshCb = refreshCb;
- _userCbData = userCbData;
-}
-
void SGTerraSync::syncAirportsModels()
{
static const char* bounds = "MZAJKL"; // airport sync order: K-L, A-J, M-Z
{
ostringstream dir;
dir << "Airports/" << synced_other;
- WaitingTile w(dir.str(),false);
+ SyncItem w(dir.str(), SyncItem::AirportData);
_svnThread->request( w );
}
}
- WaitingTile w("Models",false);
+
+ SyncItem w("Models", SyncItem::SharedModels);
_svnThread->request( w );
}
-
-void SGTerraSync::syncArea( int lat, int lon )
+void SGTerraSync::syncAreaByPath(const std::string& aPath)
{
- if ( lat < -90 || lat > 90 || lon < -180 || lon > 180 )
- return;
- char NS, EW;
- int baselat, baselon;
-
- if ( lat < 0 ) {
- int base = (int)(lat / 10);
- if ( lat == base * 10 ) {
- baselat = base * 10;
- } else {
- baselat = (base - 1) * 10;
- }
- NS = 's';
- } else {
- baselat = (int)(lat / 10) * 10;
- NS = 'n';
- }
- if ( lon < 0 ) {
- int base = (int)(lon / 10);
- if ( lon == base * 10 ) {
- baselon = base * 10;
- } else {
- baselon = (base - 1) * 10;
- }
- EW = 'w';
- } else {
- baselon = (int)(lon / 10) * 10;
- EW = 'e';
- }
-
- const char* terrainobjects[3] = { "Terrain", "Objects", 0 };
- bool refresh=true;
+ const char* terrainobjects[3] = { "Terrain/", "Objects/", 0 };
for (const char** tree = &terrainobjects[0]; *tree; tree++)
{
- ostringstream dir;
- dir << *tree << "/" << setfill('0')
- << EW << setw(3) << abs(baselon) << NS << setw(2) << abs(baselat) << "/"
- << EW << setw(3) << abs(lon) << NS << setw(2) << abs(lat);
- WaitingTile w(dir.str(),refresh);
+ std::string dir = string(*tree) + aPath;
+ if (_activeTileDirs.find(dir) != _activeTileDirs.end()) {
+ continue;
+ }
+
+ _activeTileDirs.insert(dir);
+ SyncItem w(dir, SyncItem::Tile);
_svnThread->request( w );
- refresh=false;
}
}
+bool SGTerraSync::scheduleTile(const SGBucket& bucket)
+{
+ std::string basePath = bucket.gen_base_path();
+ syncAreaByPath(basePath);
+ return true;
+}
-void SGTerraSync::syncAreas( int lat, int lon, int lat_dir, int lon_dir )
+bool SGTerraSync::isTileDirPending(const std::string& sceneryDir) const
{
- if ( lat_dir == 0 && lon_dir == 0 ) {
- // do surrounding 8 1x1 degree areas.
- for ( int i = lat - 1; i <= lat + 1; ++i ) {
- for ( int j = lon - 1; j <= lon + 1; ++j ) {
- if ( i != lat || j != lon ) {
- syncArea( i, j );
- }
- }
- }
- } else {
- if ( lat_dir != 0 ) {
- syncArea( lat + lat_dir, lon - 1 );
- syncArea( lat + lat_dir, lon + 1 );
- syncArea( lat + lat_dir, lon );
- }
- if ( lon_dir != 0 ) {
- syncArea( lat - 1, lon + lon_dir );
- syncArea( lat + 1, lon + lon_dir );
- syncArea( lat, lon + lon_dir );
+ if (!_svnThread->_running) {
+ return false;
+ }
+
+ const char* terrainobjects[3] = { "Terrain/", "Objects/", 0 };
+ for (const char** tree = &terrainobjects[0]; *tree; tree++) {
+ string s = *tree + sceneryDir;
+ if (_activeTileDirs.find(s) != _activeTileDirs.end()) {
+ return true;
}
}
- // do current 1x1 degree area first
- syncArea( lat, lon );
+ return false;
}
-
-bool SGTerraSync::schedulePosition(int lat, int lon)
+void SGTerraSync::scheduleDataDir(const std::string& dataDir)
{
- bool Ok = false;
+ if (_activeTileDirs.find(dataDir) != _activeTileDirs.end()) {
+ return;
+ }
- // Ignore messages where the location does not change
- if ( lat != last_lat || lon != last_lon )
- {
- if (_svnThread->_running)
- {
- SG_LOG(SG_TERRAIN,SG_DEBUG, "Requesting scenery update for position " <<
- lat << "," << lon);
- int lat_dir=0;
- int lon_dir=0;
- if ( last_lat != NOWHERE && last_lon != NOWHERE )
- {
- int dist = lat - last_lat;
- if ( dist != 0 )
- {
- lat_dir = dist / abs(dist);
- }
- else
- {
- lat_dir = 0;
- }
- dist = lon - last_lon;
- if ( dist != 0 )
- {
- lon_dir = dist / abs(dist);
- } else
- {
- lon_dir = 0;
- }
- }
+ _activeTileDirs.insert(dataDir);
+ SyncItem w(dataDir, SyncItem::AIData);
+ _svnThread->request( w );
- SG_LOG(SG_TERRAIN,SG_DEBUG, "Scenery update for " <<
- "lat = " << lat << ", lon = " << lon <<
- ", lat_dir = " << lat_dir << ", " <<
- "lon_dir = " << lon_dir);
+}
- syncAreas( lat, lon, lat_dir, lon_dir );
- Ok = true;
- }
- last_lat = lat;
- last_lon = lon;
+bool SGTerraSync::isDataDirPending(const std::string& dataDir) const
+{
+ if (!_svnThread->_running) {
+ return false;
}
- return Ok;
+ return (_activeTileDirs.find(dataDir) != _activeTileDirs.end());
}
+void SGTerraSync::reposition()
+{
+ // stub, remove
+}