From f3679f121d9e53fe163efd545fdcd2af18bf3ad9 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Mon, 15 Feb 2021 15:51:23 +0000 Subject: [PATCH] Allow replay of Continuous recordings if --load-tape is given a URL. E.g. for --load-tape=http[s]://foo.com/foo/bar/wibble.fgtape, we download in the background to a file called foo.com_[MD5]_wibble.fgtape, where [MD5] is an 8-character hash of /foo/bar. We assume any existing file contains valid data and only download any remaining data (by specifying an http Range header). Also, when loading/downloading and replaying continuous recordings at startup, we set the aircraft and airport from the recording. src/Aircraft/replay.cxx src/Aircraft/replay.hxx FGReplay::loadContinuousHeader() Loads properties from Continuous recording's header, distinguishing between failure due to incorrect header, or due to a truncated file. FGReplay::indexContinuousRecording() Contains Continuous recording indexing code, in a form that can be used in background while we are downloading. Added a mutex to protect m_continuous_in_time_to_frameinfo, which can now be modified in background as Continuous recording is downloaded. src/Main/fg_init.cxx src/Main/options.cxx fgOptLoadTape(): Modified to handle --load-tape=. We start download, and read the header before returning, so that we can force the FDM to use the recording's aircraft instead of the user's default. Limit recording download rate if /sim/replay/download-max-bytes-per-sec is set, by calling new filerequest->setMaxBytesPerSec(). --- src/Aircraft/replay.cxx | 333 +++++++++++++++++++++++++--------------- src/Aircraft/replay.hxx | 43 +++++- src/Main/fg_init.cxx | 2 +- src/Main/options.cxx | 199 ++++++++++++++++++++---- 4 files changed, 418 insertions(+), 159 deletions(-) diff --git a/src/Aircraft/replay.cxx b/src/Aircraft/replay.cxx index 95c29551a..1d1f308e6 100644 --- a/src/Aircraft/replay.cxx +++ b/src/Aircraft/replay.cxx @@ -1239,6 +1239,8 @@ FGReplay::replay( double time ) { replayMessage(time); + std::lock_guard lock(m_continuous_in_time_to_frameinfo_lock); + if (!m_continuous_in_time_to_frameinfo.empty()) { // We are replaying a continuous recording. // @@ -1615,6 +1617,7 @@ void FGReplay::replay( double FGReplay::get_start_time() { + std::lock_guard lock(m_continuous_in_time_to_frameinfo_lock); if (!m_continuous_in_time_to_frameinfo.empty()) { double ret = m_continuous_in_time_to_frameinfo.begin()->first; SG_LOG(SG_SYSTEMS, SG_DEBUG, @@ -1645,6 +1648,7 @@ FGReplay::get_start_time() double FGReplay::get_end_time() { + std::lock_guard lock(m_continuous_in_time_to_frameinfo_lock); if (!m_continuous_in_time_to_frameinfo.empty()) { double ret = m_continuous_in_time_to_frameinfo.rbegin()->first; SG_LOG(SG_SYSTEMS, SG_DEBUG, @@ -1824,142 +1828,225 @@ FGReplay::saveTape(const SGPropertyNode* Extra) } +int FGReplay::loadContinuousHeader(const std::string& path, std::istream* in, SGPropertyNode* properties) +{ + std::ifstream in0; + if (!in) { + in0.open(path); + in = &in0; + } + if (!*in) { + SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to open path=" << path); + return +1; + } + std::vector buffer(strlen( FlightRecorderFileMagic) + 1); + in->read(&buffer.front(), buffer.size()); + SG_LOG(SG_SYSTEMS, SG_DEBUG, "in->gcount()=" << in->gcount() << " buffer.size()=" << buffer.size()); + if ((size_t) in->gcount() != buffer.size()) { + // Further download is needed. + return +1; + } + if (strcmp(&buffer.front(), FlightRecorderFileMagic)) { + SG_LOG(SG_SYSTEMS, SG_DEBUG, "fgtape prefix doesn't match FlightRecorderFileMagic in path: " << path); + return -1; + } + bool ok = false; + try { + PropertiesRead(*in, properties); + ok = true; + } + catch (std::exception& e) { + SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to read Config properties in: " << path); + } + if (!ok) { + // Failed to read properties, so indicate that further download is needed. + return +1; + } + SG_LOG(SG_SYSTEMS, SG_BULK, "properties is:\n" + << writePropertiesInline(properties, true /*write_all*/) << "\n"); + return 0; +} + +void FGReplay::indexContinuousRecording(const void* data, size_t numbytes) +{ + SG_LOG(SG_SYSTEMS, SG_DEBUG, "Indexing Continuous recording " + << " data=" << data + << " numbytes=" << numbytes + << " m_continuous_indexing_pos=" << m_continuous_indexing_pos + << " m_continuous_in_time_to_frameinfo.size()=" << m_continuous_in_time_to_frameinfo.size() + ); + time_t t0 = time(NULL); + std::streampos original_pos = m_continuous_indexing_pos; + size_t original_num_frames = m_continuous_in_time_to_frameinfo.size(); + + // Reset any EOF because there might be new data. + m_continuous_indexing_in.clear(); + + for(;;) + { + SG_LOG(SG_SYSTEMS, SG_BULK, "reading frame." + << " m_continuous_in.tellg()=" << m_continuous_in.tellg() + ); + m_continuous_indexing_in.seekg(m_continuous_indexing_pos); + double sim_time; + m_continuous_indexing_in.read(reinterpret_cast(&sim_time), sizeof(sim_time)); + + SG_LOG(SG_SYSTEMS, SG_BULK, "" + << " m_continuous_indexing_pos=" << m_continuous_indexing_pos + << " m_continuous_indexing_in.tellg()=" << m_continuous_indexing_in.tellg() + << " sim_time=" << sim_time + ); + FGFrameInfo frameinfo; + frameinfo.offset = m_continuous_indexing_pos; + + auto datas = m_continuous_in_config->getChildren("data"); + SG_LOG(SG_SYSTEMS, SG_BULK, "datas.size()=" << datas.size()); + for (auto data: datas) { + uint32_t length; + m_continuous_indexing_in.read(reinterpret_cast(&length), sizeof(length)); + SG_LOG(SG_SYSTEMS, SG_BULK, + "m_continuous_in.tellg()=" << m_continuous_indexing_in.tellg() + << " Skipping data_type=" << data->getStringValue() + << " length=" << length + ); + // Move forward bytes. + m_continuous_indexing_in.seekg(length, std::ios_base::cur); + if (length) { + if (!strcmp(data->getStringValue(), "signals")) { + frameinfo.has_signals = true; + } + else if (!strcmp(data->getStringValue(), "multiplayer")) { + frameinfo.has_multiplayer = true; + ++m_num_frames_multiplayer; + } + else if (!strcmp(data->getStringValue(), "extra-properties")) { + frameinfo.has_extra_properties = true; + ++m_num_frames_extra_properties; + } + } + } + SG_LOG(SG_SYSTEMS, SG_BULK, "" + << " pos=" << m_continuous_indexing_pos + << " sim_time=" << sim_time + << " m_num_frames_multiplayer=" << m_num_frames_multiplayer + << " m_num_frames_extra_properties=" << m_num_frames_extra_properties + ); + + if (!m_continuous_indexing_in) { + // Failed to read a frame, e.g. because of EOF. Leave + // m_continuous_indexing_pos unchanged so we can try again at same + // starting position if recording is upated by background download. + // + SG_LOG(SG_SYSTEMS, SG_BULK, "m_continuous_indexing_in failed, giving up"); + break; + } + + // We have successfully read a frame, so add it to + // m_continuous_in_time_to_frameinfo[]. + // + m_continuous_indexing_pos = m_continuous_indexing_in.tellg(); + std::lock_guard lock(m_continuous_in_time_to_frameinfo_lock); + m_continuous_in_time_to_frameinfo[sim_time] = frameinfo; + } + time_t t = time(NULL) - t0; + auto new_bytes = m_continuous_indexing_pos - original_pos; + auto num_frames = m_continuous_in_time_to_frameinfo.size(); + auto num_new_frames = num_frames - original_num_frames; + if (num_new_frames) { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Continuous recording: index updated:" + << " num_frames=" << num_frames + << " num_new_frames=" << num_new_frames + << " new_bytes=" << new_bytes + ); + } + SG_LOG(SG_SYSTEMS, SG_DEBUG, "Indexed uncompressed recording." + << " time taken=" << t << "s." + << " num_new_frames=" << num_new_frames + << " m_continuous_indexing_pos=" << m_continuous_indexing_pos + << " m_continuous_in_time_to_frameinfo.size()=" << m_continuous_in_time_to_frameinfo.size() + << " m_num_frames_multiplayer=" << m_num_frames_multiplayer + << " m_num_frames_extra_properties=" << m_num_frames_extra_properties + ); + // Probably don't need this lock because we're only reading + // m_continuous_in_time_to_frameinfo, and nothing else can be writing it. + // + std::lock_guard lock(m_continuous_in_time_to_frameinfo_lock); + fgSetInt("/sim/replay/continuous-stats-num-frames", m_continuous_in_time_to_frameinfo.size()); + fgSetInt("/sim/replay/continuous-stats-num-frames-extra-properties", m_num_frames_extra_properties); + fgSetInt("/sim/replay/continuous-stats-num-frames-multiplayer", m_num_frames_multiplayer); + if (!numbytes) { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Continuous recording: indexing finished"); + m_continuous_indexing_in.close(); + } +} + /** Read a flight recorder tape with given filename from disk. * Copies MetaData's "meta" node into MetaMeta out-param. * Actual data and signal configuration is not read when in "Preview" mode. */ bool -FGReplay::loadTape(const SGPath& Filename, bool Preview, SGPropertyNode& MetaMeta) +FGReplay::loadTape(const SGPath& Filename, bool Preview, SGPropertyNode& MetaMeta, simgear::HTTP::FileRequest* filerequest) { SG_LOG(SG_SYSTEMS, SG_DEBUG, "loading Preview=" << Preview << " Filename=" << Filename); - { - /* Try to load as uncompressed Continuous recording first. */ - std::ifstream in_preview; - std::ifstream& in(Preview ? in_preview : m_continuous_in); - in.open( Filename.str()); - if (!in) { - SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to open Filename=" << Filename); - return false; - } - std::vector buffer(strlen( FlightRecorderFileMagic) + 1); - in.read(&buffer.front(), buffer.size()); - if (strcmp(&buffer.front(), FlightRecorderFileMagic)) { - SG_LOG(SG_SYSTEMS, SG_ALERT, "fgtape prefix doesn't match FlightRecorderFileMagic: '" << &buffer.front() << "'"); + + /* Try to load as uncompressed Continuous recording first. */ + std::ifstream in_preview; + std::ifstream& in(Preview ? in_preview : m_continuous_in); + in.open(Filename.str()); + if (!in) { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to open" + << " Filename=" << Filename.str() + << " in.is_open()=" << in.is_open() + ); + return false; + } + m_continuous_in_config = new SGPropertyNode; + int e = loadContinuousHeader(Filename.str(), &in, m_continuous_in_config); + if (e == 0) { + SG_LOG(SG_SYSTEMS, SG_DEBUG, "m_continuous_in_config is:\n" + << writePropertiesInline(m_continuous_in_config, true /*write_all*/) << "\n"); + copyProperties(m_continuous_in_config->getNode("meta", 0, true), &MetaMeta); + if (Preview) { in.close(); - } - else { - SG_LOG(SG_SYSTEMS, SG_DEBUG, "fgtape is uncompressed: " << Filename); - m_continuous_in_config = new SGPropertyNode; - try { - PropertiesRead(in, m_continuous_in_config.get()); - } - catch (std::exception& e) { - SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to read Config properties in: " << Filename); - in.close(); - return false; - } - SG_LOG(SG_SYSTEMS, SG_DEBUG, "m_continuous_in_config is:\n" - << writePropertiesInline(m_continuous_in_config, true /*write_all*/) << "\n"); - copyProperties(m_continuous_in_config->getNode("meta", 0, true), &MetaMeta); - if (Preview) { - in.close(); - return true; - } - m_pRecorder->reinit(m_continuous_in_config); - clear(); - fillRecycler(); - time_t t = time(NULL); - size_t pos = 0; - m_continuous_in_time_last = -1; - m_continuous_in_time_to_frameinfo.clear(); - int num_frames_extra_properties = 0; - int num_frames_multiplayer = 0; - // Read entire recording and build up in-memory cache of simulator - // time to file offset, so we can handle random access. - // - // We also cache any frames that modify extra-properties. - // - SG_LOG(SG_SYSTEMS, SG_DEBUG, "Indexing Continuous recording " << Filename); - for(;;) - { - SG_LOG(SG_SYSTEMS, SG_BULK, "reading frame." - << " m_continuous_in.tellg()=" << m_continuous_in.tellg() - ); - pos = m_continuous_in.tellg(); - m_continuous_in.seekg(pos); - double sim_time; - m_continuous_in.read(reinterpret_cast(&sim_time), sizeof(sim_time)); - - SG_LOG(SG_SYSTEMS, SG_BULK, - "pos=" << pos - << " m_continuous_in.tellg()=" << m_continuous_in.tellg() - << " sim_time=" << sim_time - ); - FGFrameInfo frameinfo; - frameinfo.offset = pos; - - //bool frame_has_property_changes = false; - auto datas = m_continuous_in_config->getChildren("data"); - SG_LOG(SG_SYSTEMS, SG_DEBUG, "datas.size()=" << datas.size()); - for (auto data: datas) { - uint32_t length; - m_continuous_in.read(reinterpret_cast(&length), sizeof(length)); - SG_LOG(SG_SYSTEMS, SG_BULK, - "m_continuous_in.tellg()=" << m_continuous_in.tellg() - << " Skipping data_type=" << data->getStringValue() - << " length=" << length - ); - m_continuous_in.seekg(length, std::ios_base::cur); - if (length) { - if (!strcmp(data->getStringValue(), "signals")) { - frameinfo.has_signals = true; - } - else if (!strcmp(data->getStringValue(), "multiplayer")) { - frameinfo.has_multiplayer = true; - ++num_frames_multiplayer; - } - else if (!strcmp(data->getStringValue(), "extra-properties")) { - frameinfo.has_extra_properties = true; - ++num_frames_extra_properties; - } - } - } - SG_LOG(SG_SYSTEMS, SG_BULK, "" - << " pos=" << pos - << " sim_time=" << sim_time - << " num_frames_multiplayer=" << num_frames_multiplayer - << " num_frames_extra_properties=" << num_frames_extra_properties - ); - - if (!m_continuous_in) { - // EOF; we need to cope if last frame is incomplete, as - // this can easily happen if Flightgear was killed while - // recording. - break; - } - - m_continuous_in_time_to_frameinfo[sim_time] = frameinfo; - } - t = time(NULL) - t; - SG_LOG(SG_SYSTEMS, SG_ALERT, "Indexed uncompressed recording" - << ". time taken: " << t << "s" - << ". recording size: " << pos - << ". Number of frames: " << m_continuous_in_time_to_frameinfo.size() - << ". num_frames_multiplayer: " << num_frames_multiplayer - << ". num_frames_extra_properties: " << num_frames_extra_properties - ); - fgSetInt("/sim/replay/continuous-stats-num-frames", m_continuous_in_time_to_frameinfo.size()); - fgSetInt("/sim/replay/continuous-stats-num-frames-extra-properties", num_frames_extra_properties); - fgSetInt("/sim/replay/continuous-stats-num-frames-multiplayer", num_frames_multiplayer); - start(true /*NewTape*/); return true; } - } + m_pRecorder->reinit(m_continuous_in_config); + clear(); + fillRecycler(); + m_continuous_in_time_last = -1; + m_continuous_in_time_to_frameinfo.clear(); + m_num_frames_extra_properties = 0; + m_num_frames_multiplayer = 0; + m_continuous_indexing_in.open(Filename.str()); + m_continuous_indexing_pos = in.tellg(); + SG_LOG(SG_SYSTEMS, SG_DEBUG, "filerequest=" << filerequest); - SG_LOG(SG_SYSTEMS, SG_DEBUG, "Filename=" << Filename); + // Make an in-memory index of the recording. + if (filerequest) { + // Always call indexContinuousRecording once in case there is + // nothing to download. + indexContinuousRecording(nullptr, 1 /*Zero means EOF. */); + filerequest->setCallback( [this](const void* data, size_t numbytes) { + SG_LOG(SG_GENERAL, SG_BULK, "calling indexContinuousRecording() data=" << data << " numbytes=" << numbytes); + indexContinuousRecording(data, numbytes); + }); + } + else { + indexContinuousRecording(nullptr, 0); + } + start(true /*NewTape*/); + return true; + } + + // Not a continuous recording. + in.close(); + if (filerequest) { + SG_LOG(SG_SYSTEMS, SG_DEBUG, "Cannot load Filename=" << Filename << " because it is download but not Continuous recording"); + return false; + } bool ok = true; - /* open input stream ********************************************/ + /* Open as a gzipped Normal recording. ********************************************/ gzContainerReader input(Filename, FlightRecorderFileMagic); if (input.eof() || !input.good()) { diff --git a/src/Aircraft/replay.hxx b/src/Aircraft/replay.hxx index 9ce21d632..ed697e2c0 100644 --- a/src/Aircraft/replay.hxx +++ b/src/Aircraft/replay.hxx @@ -34,6 +34,7 @@ #include #include #include +#include #include @@ -121,6 +122,27 @@ public: bool saveTape(const SGPropertyNode* ConfigData); bool loadTape(const SGPropertyNode* ConfigData); + + // If filerequest is set, the local file is a Continuous recording and + // it might increase in size as downloading progresses, so we need to + // incrementally index the file until the file request has finished the + // download. + // + bool loadTape( + const SGPath& Filename, + bool Preview, + SGPropertyNode& MetaMeta, + simgear::HTTP::FileRequest* filerequest=nullptr + ); + + // Attempts to load Continuous recording header properties into + // . If in is null we use internal std::fstream, otherwise we + // use *in. + // + // Returns 0 on success, +1 if we may succeed after further download, or -1 + // if recording is not a Continuous recording. + // + static int loadContinuousHeader(const std::string& path, std::istream* in, SGPropertyNode* properties); private: void clear(); @@ -159,7 +181,16 @@ private: bool listTapes(bool SameAircraftFilter, const SGPath& tapeDirectory); bool saveTape(const SGPath& Filename, SGPropertyNode_ptr MetaData); - bool loadTape(const SGPath& Filename, bool Preview, SGPropertyNode& MetaMeta); + + // Build up in-memory cache of simulator time to file offset, so we can + // handle random access. + // + // We also cache any frames that modify extra-properties. + // + // Can be called multiple times, e.g. if recording is being downlaoded. + // + void indexContinuousRecording(const void* data, size_t numbytes); + SGPropertyNode_ptr continuousWriteHeader( std::ofstream& out, const SGPath& path, @@ -215,9 +246,19 @@ private: std::ifstream m_continuous_in; bool m_continuous_in_multiplayer; bool m_continuous_in_extra_properties; + std::mutex m_continuous_in_time_to_frameinfo_lock; std::map m_continuous_in_time_to_frameinfo; SGPropertyNode_ptr m_continuous_in_config; double m_continuous_in_time_last; + + std::ifstream m_continuous_indexing_in; + std::streampos m_continuous_indexing_pos; + + // Only used for gathering statistics that are then written into + // properties. + // + int m_num_frames_extra_properties = 0; + int m_num_frames_multiplayer = 0; // For writing uncompressed fgtape file. SGPropertyNode_ptr m_continuous_out_config; diff --git a/src/Main/fg_init.cxx b/src/Main/fg_init.cxx index 68bcffef0..5b0965021 100644 --- a/src/Main/fg_init.cxx +++ b/src/Main/fg_init.cxx @@ -974,7 +974,7 @@ void fgCreateSubsystems(bool duringReset) { throw sg_io_exception("Error loading materials file", mpath); } - // may exist already due to GUI startup + // may exist already due to GUI startup or --load-tape=http... if (!globals->get_subsystem()) { globals->add_new_subsystem(); } diff --git a/src/Main/options.cxx b/src/Main/options.cxx index 97693f07a..f540e38dc 100644 --- a/src/Main/options.cxx +++ b/src/Main/options.cxx @@ -42,6 +42,8 @@ #include #include +#include +#include #include #include #include @@ -78,6 +80,7 @@ #include #include #include +#include #include "AircraftDirVisitorBase.hxx" #include @@ -1558,45 +1561,173 @@ fgOptSetProperty(const char* raw) return ret ? FG_OPTIONS_OK : FG_OPTIONS_ERROR; } +/* If is a URL, return suitable name for downloaded file. */ +static std::string urlToLocalPath(const char* url) +{ + bool http = simgear::strutils::starts_with(url, "http://"); + bool https = simgear::strutils::starts_with(url, "https://"); + if (!http && !https) { + return ""; + } + // e.g. http://fg.com/foo/bar/wibble.fgtape + const char* s2 = (http) ? url+7 : url+8; // fg.com/foo/bar/wibble.fgtape + const char* s3 = strchr(s2, '/'); // /foo/bar/wibble.fgtape + const char* s4 = (s3) ? strrchr(s3, '/') : NULL; // /wibble.fgtape + std::string path; + if (s3) path = std::string(s2, s3-s2); // fg.com + path += '_'; // fg.com_ + if (s3 && s4 > s3) { + path += simgear::strutils::md5(s3, s4-s3).substr(0, 8); + path += '_'; // fg.com_12345678_ + } + if (s4) path += (s4+1); // fg.com_12345678_wibble.fgtape + if (!simgear::strutils::ends_with(path, ".fgtape")) path += ".fgtape"; + return path; +} + + static int fgOptLoadTape(const char* arg) { - // load a flight recorder tape but wait until the fdm is initialized - class DelayedTapeLoader : SGPropertyChangeListener { - public: - DelayedTapeLoader( const char * tape ) : - _tape(SGPath::fromUtf8(tape)) - { - SGPropertyNode_ptr n = fgGetNode("/sim/signals/fdm-initialized", true); - n->addChangeListener( this ); + // load a flight recorder tape but wait until the fdm is initialized. + // + struct DelayedTapeLoader : SGPropertyChangeListener { + + DelayedTapeLoader( const char * tape, simgear::HTTP::FileRequest* filerequest) : + _tape(SGPath::fromUtf8(tape)), + _filerequest(filerequest) + { + fgGetNode("/sim/signals/fdm-initialized", true)->addChangeListener( this ); + } + + virtual ~ DelayedTapeLoader() {} + + virtual void valueChanged(SGPropertyNode * node) + { + if (!fgGetBool("/sim/signals/fdm-initialized")) { + return; + } + fgGetNode("/sim/signals/fdm-initialized", true)->removeChangeListener( this ); + + // tell the replay subsystem to load the tape + FGReplay* replay = globals->get_subsystem(); + assert(replay); + SGPropertyNode_ptr arg = new SGPropertyNode(); + arg->setStringValue("tape", _tape.utf8Str() ); + arg->setBoolValue( "same-aircraft", 0 ); + if (!replay->loadTape(_tape, false /*preview*/, *arg, _filerequest)) { + // Force shutdown if we can't load tape specified on command-line. + SG_LOG(SG_GENERAL, SG_POPUP, "Exiting because unable to load fgtape: " << _tape.str()); + flightgear::modalMessageBox("Exiting because unable to load fgtape", _tape.str(), ""); + fgOSExit(1); + } + delete this; // commence suicide + } + private: + SGPath _tape; + simgear::HTTP::FileRequest* _filerequest; + }; + + SGPropertyNode_ptr properties(new SGPropertyNode); + simgear::HTTP::FileRequest* filerequest = nullptr; + + std::string path = urlToLocalPath(arg); + if (path == "") { + // is a local file. + // + // Load the recording's header if it is a Continuous recording. + // + (void) FGReplay::loadContinuousHeader(arg, nullptr /*in*/, properties); + } + else { + // is a URL. Start download. + // + // Load the recording's header if it is a Continuous recording. + // + // This is a little messy - we need to create a FGHTTPClient subsystem + // in order to do the download, and we call its update() method + // directly in order to download at least the header. + // + const char* url = arg; + FGHTTPClient* http = globals->add_new_subsystem(); + http->init(); + filerequest = new simgear::HTTP::FileRequest(url, path, true /*append*/); + long max_download_speed = fgGetLong("/sim/replay/download-max-bytes-per-sec"); + if (max_download_speed != 0) { + // Can be useful to limite download speed for testing background + // download. + // + SG_LOG(SG_GENERAL, SG_ALERT, "Limiting download speed" + << " /sim/replay/download-max-bytes-per-sec=" << max_download_speed + ); + filerequest->setMaxBytesPerSec(max_download_speed); + } + http->client()->makeRequest(filerequest); + SG_LOG(SG_GENERAL, SG_DEBUG, "" + << " filerequest->responseCode()=" << filerequest->responseCode() + << " filerequest->responseReason()=" << filerequest->responseReason() + ); + + // Load recording header, looping so that we wait for the initial + // portion of the recording to be downloaded. We give up after a fixed + // timeout. + // + time_t t0 = time(NULL); + for(;;) { + // Run http client's update() to download any pending data. + http->update(0); + + // Try to load properties from recording header. + int e = FGReplay::loadContinuousHeader(path, nullptr /*in*/, properties); + if (e == 0) { + // Success. We leave active - it will carry + // on downloading when the main update loop gets going + // later. Hopefully the delay before that happens will not + // cause a server timeout. + // + break; + } + if (e == -1) { + SG_LOG(SG_GENERAL, SG_POPUP, "Not a Continuous recording: url=" << url << " local filename=" << path); + // Replay from URL only works with Continuous recordings. + return FG_OPTIONS_EXIT; + } + + // If we get here, need to download some more. + if (time(NULL) - t0 > 30) { + SG_LOG(SG_GENERAL, SG_POPUP, "Timeout while reading downloaded recording from " << url << ". local path=" << path); + return FG_OPTIONS_EXIT; + } + sleep(1); + } + } + + // Set aircraft from recording header if we loaded it above; this has to + // happen now, before the FDM is initialised. Also set the airport; we + // don't actually have to do this because the replay doesn't need terrain + // to work, but we might as well load the correct terrain. + // + std::string aircraft = properties->getStringValue("meta/aircraft-type"); + std::string airport = properties->getStringValue("meta/closest-airport-id"); + SG_LOG(SG_GENERAL, SG_ALERT, "From recording header: aircraft=" << aircraft << " airport=" << airport); + if (aircraft != "") { + // Force --aircraft and --airport options to use values from the + // recording. + // + Options::sharedInstance()->setOption("aircraft", aircraft); + } + if (airport != "") { + // Looks like setting --airport option doesn't work - we need to call + // fgOptAirport() directly. + // + Options::sharedInstance()->setOption("airport", airport); + fgOptAirport(airport.c_str()); } - virtual ~ DelayedTapeLoader() {} - - virtual void valueChanged(SGPropertyNode * node) - { - node->removeChangeListener( this ); - - // tell the replay subsystem to load the tape - FGReplay* replay = globals->get_subsystem(); - SGPropertyNode_ptr arg = new SGPropertyNode(); - arg->setStringValue("tape", _tape.utf8Str() ); - arg->setBoolValue( "same-aircraft", 0 ); - if (!replay->loadTape(arg)) { - // Force shutdown. - SG_LOG(SG_GENERAL, SG_POPUP, "Exiting because unable to load fgtape: " << _tape.str()); - flightgear::modalMessageBox("Exiting because unable to load fgtape", _tape.str(), ""); - fgOSExit(1); - } - delete this; // commence suicide - } - private: - SGPath _tape; - - }; - - new DelayedTapeLoader(arg); - return FG_OPTIONS_OK; + // Arrange to load the recording after FDM has initialised. + new DelayedTapeLoader(path.c_str(), filerequest); + + return FG_OPTIONS_OK; } static int fgOptDisableGUI(const char*)