18#include <simgear/debug/ErrorReportingCallback.hxx>
19#include <simgear/debug/LogCallback.hxx>
20#include <simgear/timing/timestamp.hxx>
22#include <simgear/io/iostreams/sgstream.hxx>
23#include <simgear/structure/commands.hxx>
40const double MinimumIntervalBetweenDialogs = 5.0;
41const double NoNewErrorsTimeout = 8.0;
46using PerThreadErrorContextStack = std::map<std::string, string_list>;
50static thread_local PerThreadErrorContextStack thread_errorContextStack;
55enum class Aggregation {
75 "error-missing-shader",
76 "error-loading-texture",
77 "error-xml-model-load",
78 "error-3D-model-load",
80 "error-scenario-load",
82 "error-audio-fx-load",
83 "error-xml-load-command",
84 "error-aircraft-systems",
85 "error-input-device-config",
86 "error-ai-traffic-schedule",
91 "error-type-not-found",
92 "error-type-out-of-memory",
93 "error-type-bad-header",
94 "error-type-bad-data",
95 "error-type-misconfigured",
97 "error-type-network"};
101 "error-category-aircraft",
102 "error-category-aircraft-from-hangar",
103 "error-category-custom-scenery",
104 "error-category-terrasync",
105 "error-category-addon",
106 "error-category-scenario",
107 "error-category-input-device",
108 "error-category-fgdata",
109 "error-category-multiplayer",
110 "error-category-unknown",
111 "error-category-out-of-memory",
112 "error-category-traffic",
113 "error-category-shaders"};
115class RecentLogCallback :
public simgear::LogCallback
118 RecentLogCallback() : simgear::LogCallback(SG_ALL, SG_INFO)
122 bool doProcessEntry(
const simgear::LogEntry& e)
override
124 std::ostringstream os;
125 if (e.file !=
nullptr) {
126 os << e.file <<
":" << e.line <<
":\t";
132 std::lock_guard<std::mutex>
g(_lock);
133 _recentLogEntries.push_back(os.str());
135 while (_recentLogEntries.size() > _preceedingLogMessageCount) {
136 _recentLogEntries.pop_front();
144 std::lock_guard<std::mutex>
g(_lock);
146 string_list r(_recentLogEntries.begin(), _recentLogEntries.end());
151 mutable std::mutex _lock;
152 size_t _preceedingLogMessageCount = 6;
154 using LogDeque = std::deque<string>;
155 LogDeque _recentLogEntries;
158std::string lastPathComponent(
const std::string& d)
160 const auto lastSlash = d.rfind(
'/');
161 return d.substr(lastSlash+1);
255 AggregateErrors::iterator
getAggregate(Aggregation ag,
const std::string& param = {});
258 void collectError(simgear::LoadFailure type, simgear::ErrorCode code,
const std::string& details,
const sg_location& location)
263 for (
const auto& c : thread_errorContextStack) {
264 occurrence.context[c.first] = c.second.back();
269 std::lock_guard<std::mutex> g(
_lock);
273 if (!it->addOccurence(occurrence)) {
278 SG_LOG(SG_GENERAL, SG_WARN,
"Error:" << static_errorTypeIds.at(
static_cast<int>(type)) <<
" from " << static_errorIds.at(
static_cast<int>(code)) <<
"::" << details <<
"\n\t" << location.asString());
280 it->lastErrorTime.stamp();
283 const auto ty = it->type;
285 if ((ty == Aggregation::OutOfMemory) || (ty == Aggregation::InputDevice)) {
286 it->isCritical =
true;
292 it->isCritical =
true;
295 if (code == simgear::ErrorCode::LoadEffectsShaders) {
296 it->isCritical =
true;
302 if (value ==
"POP") {
303 auto it = thread_errorContextStack.find(key);
304 assert(it != thread_errorContextStack.end());
305 assert(!it->second.empty());
306 it->second.pop_back();
307 if (it->second.empty()) {
308 thread_errorContextStack.erase(it);
311 thread_errorContextStack[key].push_back(value);
317 const int catId =
static_cast<int>(
report.type);
318 auto catLabel =
globals->get_locale()->getLocalizedString(static_categoryIds.at(catId),
"sys");
320 catLabel = simgear::strutils::replace(catLabel,
"%VALUE%",
report.parameter);
325 auto ns =
globals->get_locale()->getLocalizedString(
"error-next-steps",
"sys");
332 std::ostringstream detailsTextStream;
335 for (
const auto& e :
report.errors) {
336 SGPropertyNode_ptr errNode =
_displayNode->addChild(
"error");
337 const auto em =
globals->get_locale()->getLocalizedString(static_errorIds.at(
static_cast<int>(e.code)),
"sys");
338 errNode->setStringValue(
"message", em);
339 errNode->setIntValue(
"code",
static_cast<int>(e.code));
341 const auto et =
globals->get_locale()->getLocalizedString(static_errorTypeIds.at(
static_cast<int>(e.type)),
"sys");
342 errNode->setStringValue(
"type-message", et);
343 errNode->setIntValue(
"type",
static_cast<int>(e.type));
344 errNode->setStringValue(
"details", e.detailedInfo);
346 detailsTextStream << em <<
": " << et <<
"\n";
347 detailsTextStream <<
"(" << e.detailedInfo <<
")\n";
349 if (e.origin.isValid()) {
350 errNode->setStringValue(
"location", e.origin.asString());
351 detailsTextStream <<
" from:" << e.origin.asString() <<
"\n";
354 detailsTextStream <<
"\n";
357 _displayNode->setStringValue(
"details-text", detailsTextStream.str());
360 report.haveShownToUser =
true;
364 if (a.type != report.type) return false;
365 return report.parameter.empty() ? true : report.parameter == a.parameter;
376 const int catId =
static_cast<int>(
report.type);
393 -> AggregateErrors::iterator
398 if (oc.type == simgear::LoadFailure::OutOfMemory) {
402 if (oc.hasContextKey(
"primary-aircraft")) {
403 const auto fullId =
fgGetString(
"/sim/aircraft-id");
406 const auto aircraftDirName = lastPathComponent(
fgGetString(
"/sim/aircraft-dir"));
409 return getAggregate(Aggregation::MainAircraft, aircraftDirName);
412 return getAggregate(Aggregation::HangarAircraft, aircraftDirName);
415 if (oc.hasContextKey(
"multiplayer")) {
421 if (oc.code == simgear::ErrorCode::AITrafficSchedule) {
425 if (oc.hasContextKey(
"traffic-aircraft-callsign")) {
431 if (oc.code == simgear::ErrorCode::TerraSync) {
435 if (oc.hasContextKey(
"terrain-stg") || oc.hasContextKey(
"btg")) {
443 auto path = oc.hasContextKey(
"terrain-stg") ? oc.getContextValue(
"terrain-stg") : oc.getContextValue(
"btg");
446 for (
const auto& sceneryPath :
globals->get_fg_scenery()) {
447 const auto pathStr = sceneryPath.utf8Str();
448 if (simgear::strutils::starts_with(path, pathStr)) {
449 return getAggregate(Aggregation::CustomScenery, pathStr);
466 if (oc.hasContextKey(
"scenario-path")) {
467 const auto scenarioPath = oc.getContextValue(
"scenario-path");
468 return getAggregate(Aggregation::Scenario, scenarioPath);
471 if (oc.hasContextKey(
"input-device")) {
472 return getAggregate(Aggregation::InputDevice, oc.getContextValue(
"input-device"));
479 const auto fullId =
fgGetString(
"/sim/aircraft-id");
480 const auto aircraftDirName = lastPathComponent(
fgGetString(
"/sim/aircraft-dir"));
483 return getAggregate(Aggregation::MainAircraft, aircraftDirName);
486 return getAggregate(Aggregation::HangarAircraft, aircraftDirName);
490 if (oc.code == simgear::ErrorCode::GUIDialog) {
493 const auto aircraftDirName = lastPathComponent(
fgGetString(
"/sim/aircraft-dir"));
494 return getAggregate(Aggregation::MainAircraft, aircraftDirName);
504 if (oc.code == simgear::ErrorCode::LoadEffectsShaders) {
506 return getAggregate(Aggregation::ShadersEffects, oc.getContextValue(
"effect"));
513 const auto aircraftDirName = lastPathComponent(
fgGetString(
"/sim/aircraft-dir"));
514 return getAggregate(Aggregation::MainAircraft, aircraftDirName);
521 -> AggregateErrors::iterator
524 if (a.type != ag) return false;
525 return param.empty() ? true : param == a.parameter;
541 os <<
"FlightGear " << VERSION <<
" error report, created at ";
544 time_t now = time(
nullptr);
545 strftime(buf,
sizeof(buf),
"%a, %d %b %Y %H:%M:%S GMT", gmtime(&now));
546 os << buf << std::endl;
549 os <<
"Category:" << static_categoryIds.at(
static_cast<int>(
report.type)) << std::endl;
550 if (!
report.parameter.empty()) {
551 os <<
"\tParameter:" <<
report.parameter << std::endl;
559 for (
const auto& err :
report.errors) {
560 os <<
"Error " << index++ << std::endl;
561 os <<
"\tcode:" << static_errorIds.at(
static_cast<int>(err.code)) << std::endl;
562 os <<
"\ttype:" << static_errorTypeIds.at(
static_cast<int>(err.type)) << std::endl;
564 strftime(whenBuf,
sizeof(whenBuf),
"%H:%M:%S GMT", gmtime(&err.when));
565 os <<
"\twhen:" << whenBuf << std::endl;
567 os <<
"\t" << err.detailedInfo << std::endl;
568 os <<
"\tlocation:" << err.origin.asString() << std::endl;
574 os <<
"Command line / launcher / fgfsrc options" << std::endl;
576 os <<
"\t" << o <<
"\n";
585 os <<
"Properties:" << std::endl;
589 os <<
"\t" << ps <<
": not defined\n";
591 os <<
"\t" << ps <<
": " << node->getStringValue() <<
"\n";
600 std::lock_guard<std::mutex> g(
_lock);
603 if (args->getBoolValue(
"dont-show")) {
618 std::lock_guard<std::mutex> g(
_lock);
624 const auto numAggregates =
static_cast<int>(
_aggregated.size());
625 if (args->getBoolValue(
"next")) {
630 }
else if (args->getBoolValue(
"previous")) {
635 }
else if (args->hasChild(
"index")) {
648 if (!gui->getDialog(
"error-report")) {
649 gui->showDialog(
"error-report");
663 const string where = args->getStringValue(
"where");
668 time_t now = time(
nullptr);
669 strftime(buf,
sizeof(buf),
"%Y%m%d", gmtime(&now));
673 if (where.empty() || (where ==
"!desktop")) {
674 SGPath
p = SGPath::desktop() / (
"flightgear_error_" + when +
".txt");
677 p = SGPath::desktop() / (
"flightgear_error_" + when +
"_" + std::to_string(uniqueCount++) +
".txt");
680 sg_ofstream f(
p, std::ios_base::out);
682 }
else if (where ==
"!clipboard") {
683 std::ostringstream os;
694 os <<
"\tcontext:\n";
695 for (
const auto& c : error.
context) {
696 os <<
"\t\t" << c.first <<
" = " << c.second <<
"\n";
702 os <<
"\tpreceeding log:\n";
703 for (
const auto& c : error.
log) {
704 os <<
"\t\t" << c <<
"\n";
713 return (ext.code == err.code) &&
714 (ext.type == err.type) &&
715 (ext.detailedInfo == err.detailedInfo) &&
716 (ext.origin.asString() == err.origin.asString());
731 return pos != std::string::npos;
743 const auto pos = path.find(
"Aircraft/");
758 d->_logCallback.reset(
new RecentLogCallback);
761 d->_significantProperties = {
765 "/sim/rendering/gl-info/gl-vendor",
766 "/sim/rendering/gl-info/gl-renderer",
767 "/sim/rendering/gl-info/gl-version",
768 "/sim/rendering/gl-info/gl-shading-language-version",
769 "/sim/rendering/gl-info/gl-max-texture-size",
770 "/sim/rendering/gl-info/gl-max-texture-units",
772 "/sim/rendering/preset-description",
773 "/sim/rendering/photoscenery/enabled",
774 "/sim/rendering/hdr/compute",
776 "/sim/rendering/max-paged-lod",
777 "/sim/rendering/multithreading-mode",
786 if (d->_logCallbackRegistered) {
787 sglog().removeCallback(d->_logCallback.get());
793 SGPropertyNode_ptr n =
fgGetNode(
"/sim/error-report",
true);
795 d->_enabledNode = n->getNode(
"enabled",
true);
796 if (!d->_enabledNode->hasValue()) {
797 d->_enabledNode->setBoolValue(
false);
800 d->_displayNode = n->getNode(
"display",
true);
801 d->_activeErrorNode = n->getNode(
"active",
true);
802 d->_mpReportNode = n->getNode(
"mp-report-enabled",
true);
807 d->_enabledNode.clear();
808 d->_displayNode.clear();
809 d->_activeErrorNode.clear();
815 simgear::setFailureCallback([
p](simgear::LoadFailure type, simgear::ErrorCode code,
const std::string& details,
const sg_location& location) {
816 p->collectError(type, code, details, location);
819 simgear::setErrorContextCallback([
p](
const std::string& key,
const std::string& value) {
820 p->collectContext(key, value);
823 sglog().addCallback(d->_logCallback.get());
824 d->_logCallbackRegistered =
true;
832 const auto developerMode =
fgGetBool(
"sim/developer-mode");
833 const auto disableInDeveloperMode = !d->_enabledNode->getParent()->getBoolValue(
"enable-in-developer-mode");
834 const auto dd = developerMode && disableInDeveloperMode;
836 if (dd || !d->_enabledNode) {
837 SG_LOG(SG_GENERAL, SG_INFO,
"Error reporting disabled");
838 simgear::setFailureCallback(simgear::FailureCallback());
839 simgear::setErrorContextCallback(simgear::ContextCallback());
840 if (d->_logCallbackRegistered) {
841 sglog().removeCallback(d->_logCallback.get());
842 d->_logCallbackRegistered =
false;
852 d->_fgdataPathPrefix =
globals->get_fg_root().utf8Str();
853 d->_terrasyncPathPrefix =
globals->get_terrasync_dir().utf8Str();
855 const auto aircraftPath = SGPath::fromUtf8(
fgGetString(
"/sim/aircraft-dir"));
856 d->_aircraftDirectoryName = aircraftPath.file();
861 bool showDialog =
false;
862 bool showPopup =
false;
863 bool havePendingReports =
false;
867 std::lock_guard<std::mutex> g(d->_lock);
869 if (!d->_enabledNode->getBoolValue()) {
875 d->_haveDonePostInit =
true;
877 SGTimeStamp n = SGTimeStamp::now();
880 const auto timeSinceLastDialog = (n - d->_nextShowTimeout).toSecs();
881 if (timeSinceLastDialog < MinimumIntervalBetweenDialogs) {
885 if (!d->_reportsDirty) {
889 if (d->_activeReportIndex >= 0) {
896 for (
auto&
report : d->_aggregated) {
897 if (
report.type == Aggregation::MultiPlayer) {
898 if (!d->_mpReportNode->getBoolValue()) {
900 report.haveShownToUser =
true;
904 if (
report.haveShownToUser) {
909 const auto ageSec = (n -
report.lastErrorTime).toSecs();
910 if (ageSec > NoNewErrorsTimeout) {
911 d->presentErrorToUser(
report);
918 d->sendReportToSentry(
report);
923 havePendingReports =
true;
927 if (!havePendingReports) {
928 d->_reportsDirty =
false;
945 SGPropertyNode_ptr pauseArgs(
new SGPropertyNode);
946 pauseArgs->setBoolValue(
"force-pause",
true);
947 globals->get_commands()->execute(
"do_pause", pauseArgs);
949 }
else if (showPopup) {
950 SGPropertyNode_ptr popupArgs(
new SGPropertyNode);
951 popupArgs->setIntValue(
"index", d->_activeReportIndex);
952 globals->get_commands()->execute(
"show-error-notification-popup", popupArgs,
nullptr);
958 if (d->_enabledNode) {
959 globals->get_commands()->removeCommand(
"dismiss-error-report");
960 globals->get_commands()->removeCommand(
"save-error-report-data");
961 globals->get_commands()->removeCommand(
"show-error-report");
965 const bool inReset =
fgGetBool(
"/sim/signals/reinit",
false);
967 sglog().removeCallback(d->_logCallback.get());
968 d->_logCallbackRegistered =
false;
975 auto it = thread_errorContextStack.find(key);
976 if (it == thread_errorContextStack.end())
979 return it->second.back();
987 SGSubsystemMgr::GENERAL);
SGSubsystemMgr::Registrant< flightgear::ErrorReporter > registrantErrorReporter(SGSubsystemMgr::GENERAL)
static Ptr getInstance()
Get clipboard platform specific instance.
XML-configured GUI subsystem.
virtual bool showDialog(const std::string &name)
Display a dialog box.
AggregateErrors _aggregated
SGPropertyNode_ptr _displayNode
AggregateErrors::iterator getAggregate(Aggregation ag, const std::string ¶m={})
void writeContextToStream(const ErrorOcurrence &error, std::ostream &os) const
bool isMainAircraftPath(const std::string &path) const
hueristic to identify relative paths as origination from the main aircraft as opposed to something el...
std::map< std::string, std::string > ErrorContext
bool showErrorReportCommand(const SGPropertyNode *args, SGPropertyNode *)
bool isAnyAircraftPath(const std::string &path) const
helper to determine if a file looks like it belongs to an aircraft.
SGTimeStamp _nextShowTimeout
bool dismissReportCommand(const SGPropertyNode *args, SGPropertyNode *)
void collectContext(const std::string &key, const std::string &value)
AggregateErrors::iterator getAggregateForOccurence(const ErrorOcurrence &oc)
find the appropriate agrgegate for an error, based on its context
bool saveReportCommand(const SGPropertyNode *args, SGPropertyNode *)
string_list _significantProperties
properties we want to include in reports, for debugging
SGPropertyNode_ptr _mpReportNode
void writeSignificantPropertiesToStream(std::ostream &os) const
void writeLogToStream(const ErrorOcurrence &error, std::ostream &os) const
SGPropertyNode_ptr _activeErrorNode
std::unique_ptr< RecentLogCallback > _logCallback
string _terrasyncPathPrefix
void collectError(simgear::LoadFailure type, simgear::ErrorCode code, const std::string &details, const sg_location &location)
void presentErrorToUser(AggregateReport &report)
string _aircraftDirectoryName
std::vector< AggregateReport > AggregateErrors
bool _logCallbackRegistered
void sendReportToSentry(AggregateReport &report)
void writeReportToStream(const AggregateReport &report, std::ostream &os) const
std::vector< ErrorOcurrence > OccurrenceVec
SGPropertyNode_ptr _enabledNode
void update(double dt) override
static std::string threadSpecificContextValue(const std::string &key)
static Options * sharedInstance()
std::string fgGetString(const char *name, const char *defaultValue)
Get a string value for a property.
std::vector< std::string > string_list
FlightGear Localization Support.
FlightPlan.hxx - defines a full flight-plan object, including departure, cruise, arrival information ...
void sentryReportUserError(const std::string &, const std::string &, const std::string &)
bool fgGetBool(char const *name, bool def)
Get a bool value for a property.
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
structure representing one or more errors, aggregated together
bool addOccurence(const ErrorOcurrence &err)
std::string parameter
base on type, the specific point. For example the add-on ID, AI model ident or custom scenery path
SGTimeStamp lastErrorTime
structure representing a single error which has occurred
simgear::LoadFailure type
bool hasContextKey(const std::string &key) const
std::string getContextValue(const std::string &key) const