FlightGear next
ErrorReporter.cxx
Go to the documentation of this file.
1/*
2 * SPDX-FileName: ErrorReporter.cxx
3 * SPDX-FileComment: This file is part of the program FlightGear
4 * SPDX-FileCopyrightText: Copyright (C) 2021 James Turner
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8#include "config.h"
9
10#include "ErrorReporter.hxx"
11
12#include <algorithm>
13#include <ctime> // for strftime, etc
14#include <deque>
15#include <map>
16#include <mutex>
17
18#include <simgear/debug/ErrorReportingCallback.hxx>
19#include <simgear/debug/LogCallback.hxx>
20#include <simgear/timing/timestamp.hxx>
21
22#include <simgear/io/iostreams/sgstream.hxx>
23#include <simgear/structure/commands.hxx>
24
25#include <GUI/MessageBox.hxx>
26#include <GUI/dialog.hxx>
27#include <GUI/new_gui.hxx>
28
29#include <Main/fg_props.hxx>
30#include <Main/globals.hxx>
31#include <Main/locale.hxx>
32#include <Main/options.hxx>
34#include <Scripting/NasalClipboard.hxx> // clipboard access
35
36using std::string;
37
38namespace {
39
40const double MinimumIntervalBetweenDialogs = 5.0;
41const double NoNewErrorsTimeout = 8.0;
42
43// map of context values; we allow a stack of values for
44// cases such as sub-sub-model loading where we might process repeated
45// nested model XML files
46using PerThreadErrorContextStack = std::map<std::string, string_list>;
47
48// context storage. This is per-thread so parallel osgDB threads don't
49// confuse each other
50static thread_local PerThreadErrorContextStack thread_errorContextStack;
55enum class Aggregation {
56 MainAircraft,
57 HangarAircraft, // handle hangar aircraft differently
58 CustomScenery,
59 TerraSync,
60 AddOn,
61 Scenario,
62 InputDevice,
63 FGData,
64 MultiPlayer,
65 Unknown,
66 OutOfMemory,
67 Traffic,
68 ShadersEffects
69};
70
71// these should correspond to simgear::ErrorCode enum
72// they map to translateable strings in fgdata/Translations/sys.xml
73
74string_list static_errorIds = {
75 "error-missing-shader",
76 "error-loading-texture",
77 "error-xml-model-load",
78 "error-3D-model-load",
79 "error-btg-load",
80 "error-scenario-load",
81 "error-dialog-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",
87 "error-terrasync"};
88
89string_list static_errorTypeIds = {
90 "error-type-unknown",
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",
96 "error-type-io",
97 "error-type-network"};
98
99
100string_list static_categoryIds = {
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"};
114
115class RecentLogCallback : public simgear::LogCallback
116{
117public:
118 RecentLogCallback() : simgear::LogCallback(SG_ALL, SG_INFO)
119 {
120 }
121
122 bool doProcessEntry(const simgear::LogEntry& e) override
123 {
124 std::ostringstream os;
125 if (e.file != nullptr) {
126 os << e.file << ":" << e.line << ":\t";
127 }
128
129
130 os << e.message;
131
132 std::lock_guard<std::mutex> g(_lock); // begin access to shared state
133 _recentLogEntries.push_back(os.str());
134
135 while (_recentLogEntries.size() > _preceedingLogMessageCount) {
136 _recentLogEntries.pop_front();
137 }
138
139 return true;
140 }
141
142 string_list copyRecentLogEntries() const
143 {
144 std::lock_guard<std::mutex> g(_lock); // begin access to shared state
145
146 string_list r(_recentLogEntries.begin(), _recentLogEntries.end());
147 return r;
148 }
149
150private:
151 mutable std::mutex _lock;
152 size_t _preceedingLogMessageCount = 6;
153
154 using LogDeque = std::deque<string>;
155 LogDeque _recentLogEntries;
156};
157
158std::string lastPathComponent(const std::string& d)
159{
160 const auto lastSlash = d.rfind('/');
161 return d.substr(lastSlash+1);
162}
163
164} // namespace
165
166namespace flightgear {
167
169{
170public:
171 bool _reportsDirty = false;
172 std::mutex _lock;
173 SGTimeStamp _nextShowTimeout;
174 bool _haveDonePostInit = false;
175
176 SGPropertyNode_ptr _enabledNode;
177 SGPropertyNode_ptr _displayNode;
178 SGPropertyNode_ptr _activeErrorNode;
179 SGPropertyNode_ptr _mpReportNode;
180
181 using ErrorContext = std::map<std::string, std::string>;
186 simgear::ErrorCode code;
187 simgear::LoadFailure type;
189 sg_location origin;
190 time_t when;
193
194 bool hasContextKey(const std::string& key) const
195 {
196 return context.find(key) != context.end();
197 }
198
199 std::string getContextValue(const std::string& key) const
200 {
201 auto it = context.find(key);
202 if (it == context.end())
203 return {};
204
205 return it->second;
206 }
207 };
208
209 using OccurrenceVec = std::vector<ErrorOcurrence>;
210
211 std::unique_ptr<RecentLogCallback> _logCallback;
213
217
222 bool isMainAircraftPath(const std::string& path) const;
223
224 bool isAnyAircraftPath(const std::string& path) const;
225
233 Aggregation type;
234 std::string parameter;
235 SGTimeStamp lastErrorTime;
236
237 bool haveShownToUser = false;
239 bool isCritical = false;
240
241 bool addOccurence(const ErrorOcurrence& err);
242 };
243
244
245 using AggregateErrors = std::vector<AggregateReport>;
249
253 AggregateErrors::iterator getAggregateForOccurence(const ErrorOcurrence& oc);
254
255 AggregateErrors::iterator getAggregate(Aggregation ag, const std::string& param = {});
256
257
258 void collectError(simgear::LoadFailure type, simgear::ErrorCode code, const std::string& details, const sg_location& location)
259 {
260 ErrorOcurrence occurrence{code, type, details, location, time(nullptr), string_list(), ErrorContext()};
261
262 // snapshot the top of the context stacks into our occurence data
263 for (const auto& c : thread_errorContextStack) {
264 occurrence.context[c.first] = c.second.back();
265 }
266
267 occurrence.log = _logCallback->copyRecentLogEntries();
268
269 std::lock_guard<std::mutex> g(_lock); // begin access to shared state
270 auto it = getAggregateForOccurence(occurrence);
271
272 // add to the occurence, if it's not a duplicate
273 if (!it->addOccurence(occurrence)) {
274 return; // duplicate, nothing else to do
275 }
276
277 // log it once we know it's not a duplicate
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());
279
280 it->lastErrorTime.stamp();
281 _reportsDirty = true;
282
283 const auto ty = it->type;
284 // decide if it's a critical error or not
285 if ((ty == Aggregation::OutOfMemory) || (ty == Aggregation::InputDevice)) {
286 it->isCritical = true;
287 }
288
289 // aircraft errors are critical if they occur during initial
290 // aircraft load, otherwise we just show the warning
291 if (!_haveDonePostInit && (ty == Aggregation::MainAircraft)) {
292 it->isCritical = true;
293 }
294
295 if (code == simgear::ErrorCode::LoadEffectsShaders) {
296 it->isCritical = true;
297 }
298 }
299
300 void collectContext(const std::string& key, const std::string& value)
301 {
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);
309 }
310 } else {
311 thread_errorContextStack[key].push_back(value);
312 }
313 }
314
316 {
317 const int catId = static_cast<int>(report.type);
318 auto catLabel = globals->get_locale()->getLocalizedString(static_categoryIds.at(catId), "sys");
319
320 catLabel = simgear::strutils::replace(catLabel, "%VALUE%", report.parameter);
321
322 _displayNode->setStringValue("category", catLabel);
323
324
325 auto ns = globals->get_locale()->getLocalizedString("error-next-steps", "sys");
326 _displayNode->setStringValue("next-steps", ns);
327
328
329 // remove any existing error children
330 _displayNode->removeChildren("error");
331
332 std::ostringstream detailsTextStream;
333
334 // add all the discrete errors as child nodes with all their information
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));
340
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);
345
346 detailsTextStream << em << ": " << et << "\n";
347 detailsTextStream << "(" << e.detailedInfo << ")\n";
348
349 if (e.origin.isValid()) {
350 errNode->setStringValue("location", e.origin.asString());
351 detailsTextStream << " from:" << e.origin.asString() << "\n";
352 }
353
354 detailsTextStream << "\n";
355 } // of errors within the report iteration
356
357 _displayNode->setStringValue("details-text", detailsTextStream.str());
358 _activeErrorNode->setBoolValue(true);
359
360 report.haveShownToUser = true;
361
362 // compute index; slightly clunky, find the report in _aggregated
363 auto it = std::find_if(_aggregated.begin(), _aggregated.end(), [report](const AggregateReport& a) {
364 if (a.type != report.type) return false;
365 return report.parameter.empty() ? true : report.parameter == a.parameter;
366 });
367 assert(it != _aggregated.end());
368 _activeReportIndex = static_cast<int>(std::distance(_aggregated.begin(), it));
369 _displayNode->setBoolValue("index", _activeReportIndex);
370 _displayNode->setBoolValue("have-next", _activeReportIndex < (int) _aggregated.size() - 1);
371 _displayNode->setBoolValue("have-previous", _activeReportIndex > 0);
372 }
373
375 {
376 const int catId = static_cast<int>(report.type);
377 flightgear::sentryReportUserError(static_categoryIds.at(catId),
378 report.parameter,
379 _displayNode->getStringValue());
380 }
381
382 void writeReportToStream(const AggregateReport& report, std::ostream& os) const;
383 void writeContextToStream(const ErrorOcurrence& error, std::ostream& os) const;
384 void writeLogToStream(const ErrorOcurrence& error, std::ostream& os) const;
385 void writeSignificantPropertiesToStream(std::ostream& os) const;
386
387 bool dismissReportCommand(const SGPropertyNode* args, SGPropertyNode*);
388 bool saveReportCommand(const SGPropertyNode* args, SGPropertyNode*);
389 bool showErrorReportCommand(const SGPropertyNode* args, SGPropertyNode*);
390};
391
393 -> AggregateErrors::iterator
394{
395 // all OOM errors go to a dedicated category. This is so we don't blame
396 // out of memory on the aircraft/scenery/etc, when it's not the underlying
397 // cause.
398 if (oc.type == simgear::LoadFailure::OutOfMemory) {
399 return getAggregate(Aggregation::OutOfMemory, {});
400 }
401
402 if (oc.hasContextKey("primary-aircraft")) {
403 const auto fullId = fgGetString("/sim/aircraft-id");
404
405 // we use the dir name so we combine reports from different variants, on Sentry
406 const auto aircraftDirName = lastPathComponent(fgGetString("/sim/aircraft-dir"));
407
408 if (fullId != fgGetString("/sim/aircraft")) {
409 return getAggregate(Aggregation::MainAircraft, aircraftDirName);
410 }
411
412 return getAggregate(Aggregation::HangarAircraft, aircraftDirName);
413 }
414
415 if (oc.hasContextKey("multiplayer")) {
416 return getAggregate(Aggregation::MultiPlayer, {});
417 }
418
419 // traffic cases: need to handle errors in the traffic files (schedule, rwyuse)
420 // but also errors loading aircraft models associated with traffic
421 if (oc.code == simgear::ErrorCode::AITrafficSchedule) {
422 return getAggregate(Aggregation::Traffic, {});
423 }
424
425 if (oc.hasContextKey("traffic-aircraft-callsign")) {
426 return getAggregate(Aggregation::Traffic, {});
427 }
428
429 // all TerraSync coded errors go there: this is errors for the
430 // actual download process (eg, failed to write to disk)
431 if (oc.code == simgear::ErrorCode::TerraSync) {
432 return getAggregate(Aggregation::TerraSync, {});
433 }
434
435 if (oc.hasContextKey("terrain-stg") || oc.hasContextKey("btg")) {
436 // determine if it's custom scenery, TerraSync or FGData
437
438 // bucket is no use here, we need to check the BTG/XML/STG path etc.
439 // STG is probably the best bet. This ensures if a custom scenery
440 // STG references a model, XML or texture in FGData or TerraSync
441 // incorrectly, we attribute the error to the scenery, which is
442 // likely what we want/expect
443 auto path = oc.hasContextKey("terrain-stg") ? oc.getContextValue("terrain-stg") : oc.getContextValue("btg");
444
445 // custom scenery, find out the prefix
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);
450 }
451 }
452
453 // try generic paths
454 if (simgear::strutils::starts_with(path, _fgdataPathPrefix)) {
455 return getAggregate(Aggregation::FGData, {});
456 } else if (simgear::strutils::starts_with(path, _terrasyncPathPrefix)) {
457 return getAggregate(Aggregation::TerraSync, {});
458 }
459
460
461
462 // shouldn't ever happen
463 return getAggregate(Aggregation::CustomScenery, {});
464 }
465
466 if (oc.hasContextKey("scenario-path")) {
467 const auto scenarioPath = oc.getContextValue("scenario-path");
468 return getAggregate(Aggregation::Scenario, scenarioPath);
469 }
470
471 if (oc.hasContextKey("input-device")) {
472 return getAggregate(Aggregation::InputDevice, oc.getContextValue("input-device"));
473 }
474
475 // start guessing :)
476 // from this point on we're using less reliable inferences about where the
477 // error came from, trying to avoid 'unknown'
478 if (isMainAircraftPath(oc.origin.asString())) {
479 const auto fullId = fgGetString("/sim/aircraft-id");
480 const auto aircraftDirName = lastPathComponent(fgGetString("/sim/aircraft-dir"));
481
482 if (fullId != fgGetString("/sim/aircraft")) {
483 return getAggregate(Aggregation::MainAircraft, aircraftDirName);
484 }
485
486 return getAggregate(Aggregation::HangarAircraft, aircraftDirName);
487 }
488
489 // GUI dialog errors often have no context
490 if (oc.code == simgear::ErrorCode::GUIDialog) {
491 // check if it's an aircraft dialog
492 if (isMainAircraftPath(oc.origin.asString())) {
493 const auto aircraftDirName = lastPathComponent(fgGetString("/sim/aircraft-dir"));
494 return getAggregate(Aggregation::MainAircraft, aircraftDirName);
495 }
496
497 // check if it's an add-on and use that
498 return getAggregate(Aggregation::FGData);
499 }
500
501 // becuase we report shader errors from the main thread, they don't
502 // get attributed. Collect them into their own category, which also
503 // means we can display a more specific message
504 if (oc.code == simgear::ErrorCode::LoadEffectsShaders) {
505 // we use the effect name to split shader errors
506 return getAggregate(Aggregation::ShadersEffects, oc.getContextValue("effect"));
507 }
508
509 // if we've got this far, and the path looks like an aircraft path, assume it was a cross-aircraft
510 // error, and report as such
511 // see https://gitlab.com/flightgear/flightgear/-/issues/3008
512 if (isAnyAircraftPath(oc.origin.asString())) {
513 const auto aircraftDirName = lastPathComponent(fgGetString("/sim/aircraft-dir"));
514 return getAggregate(Aggregation::MainAircraft, aircraftDirName);
515 }
516
517 return getAggregate(Aggregation::Unknown);
518}
519
520auto ErrorReporter::ErrorReporterPrivate::getAggregate(Aggregation ag, const std::string& param)
521 -> AggregateErrors::iterator
522{
523 auto it = std::find_if(_aggregated.begin(), _aggregated.end(), [ag, &param](const AggregateReport& a) {
524 if (a.type != ag) return false;
525 return param.empty() ? true : param == a.parameter;
526 });
527
528 if (it == _aggregated.end()) {
530 r.type = ag;
531 r.parameter = param;
532 _aggregated.push_back(r);
533 it = _aggregated.end() - 1;
534 }
535
536 return it;
537}
538
540{
541 os << "FlightGear " << VERSION << " error report, created at ";
542 {
543 char buf[64];
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;
547 }
548
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;
552 }
553
554 os << std::endl; // insert a blank line after header data
555
556 int index = 1;
557 char whenBuf[64];
558
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;
563
564 strftime(whenBuf, sizeof(whenBuf), "%H:%M:%S GMT", gmtime(&err.when));
565 os << "\twhen:" << whenBuf << std::endl;
566
567 os << "\t" << err.detailedInfo << std::endl;
568 os << "\tlocation:" << err.origin.asString() << std::endl;
569 writeContextToStream(err, os);
570 writeLogToStream(err, os);
571 os << std::endl; // trailing blank line
572 }
573
574 os << "Command line / launcher / fgfsrc options" << std::endl;
575 for (auto o : Options::sharedInstance()->extractOptions()) {
576 os << "\t" << o << "\n";
577 }
578 os << std::endl;
579
581}
582
584{
585 os << "Properties:" << std::endl;
586 for (const auto& ps : _significantProperties) {
587 auto node = fgGetNode(ps);
588 if (!node) {
589 os << "\t" << ps << ": not defined\n";
590 } else {
591 os << "\t" << ps << ": " << node->getStringValue() << "\n";
592 }
593 }
594 os << std::endl;
595}
596
597
598bool ErrorReporter::ErrorReporterPrivate::dismissReportCommand(const SGPropertyNode* args, SGPropertyNode*)
599{
600 std::lock_guard<std::mutex> g(_lock);
601 _activeErrorNode->setBoolValue(false);
602
603 if (args->getBoolValue("dont-show")) {
604 // TODO implement dont-show behaviour
605 }
606
607 // clear any values underneath displayNode?
608
609 _nextShowTimeout.stamp();
610 _reportsDirty = true; // set this so we check for another report to present
612
613 return true;
614}
615
616bool ErrorReporter::ErrorReporterPrivate::showErrorReportCommand(const SGPropertyNode* args, SGPropertyNode*)
617{
618 std::lock_guard<std::mutex> g(_lock);
619
620 if (_aggregated.empty()) {
621 return false;
622 }
623
624 const auto numAggregates = static_cast<int>(_aggregated.size());
625 if (args->getBoolValue("next")) {
627 if (_activeReportIndex >= numAggregates) {
628 return false;
629 }
630 } else if (args->getBoolValue("previous")) {
631 if (_activeReportIndex < 1) {
632 return false;
633 }
635 } else if (args->hasChild("index")) {
636 _activeReportIndex = args->getIntValue("index");
637 if ((_activeReportIndex < 0) || (_activeReportIndex >= numAggregates)) {
638 return false;
639 }
640 } else {
642 }
643
646
647 auto gui = globals->get_subsystem<NewGUI>();
648 if (!gui->getDialog("error-report")) {
649 gui->showDialog("error-report");
650 }
651
652 return true;
653}
654
655bool ErrorReporter::ErrorReporterPrivate::saveReportCommand(const SGPropertyNode* args, SGPropertyNode*)
656{
657 if (_activeReportIndex < 0) {
658 return false;
659 }
660
661 const auto& report = _aggregated.at(_activeReportIndex);
662
663 const string where = args->getStringValue("where");
664
665 string when;
666 {
667 char buf[64];
668 time_t now = time(nullptr);
669 strftime(buf, sizeof(buf), "%Y%m%d", gmtime(&now));
670 when = buf;
671 }
672
673 if (where.empty() || (where == "!desktop")) {
674 SGPath p = SGPath::desktop() / ("flightgear_error_" + when + ".txt");
675 int uniqueCount = 2;
676 while (p.exists()) {
677 p = SGPath::desktop() / ("flightgear_error_" + when + "_" + std::to_string(uniqueCount++) + ".txt");
678 }
679
680 sg_ofstream f(p, std::ios_base::out);
682 } else if (where == "!clipboard") {
683 std::ostringstream os;
685 NasalClipboard::getInstance()->setText(os.str());
686 }
687
688 return true;
689}
690
691
693{
694 os << "\tcontext:\n";
695 for (const auto& c : error.context) {
696 os << "\t\t" << c.first << " = " << c.second << "\n";
697 }
698}
699
701{
702 os << "\tpreceeding log:\n";
703 for (const auto& c : error.log) {
704 os << "\t\t" << c << "\n";
705 }
706}
707
709{
710 auto it = std::find_if(errors.begin(), errors.end(), [err](const ErrorOcurrence& ext) {
711 // check if the two occurences match for the purposes of
712 // de-duplication.
713 return (ext.code == err.code) &&
714 (ext.type == err.type) &&
715 (ext.detailedInfo == err.detailedInfo) &&
716 (ext.origin.asString() == err.origin.asString());
717 });
718
719 if (it != errors.end()) {
720 return false; // duplicate, don't add
721 }
722
723 errors.push_back(err);
724 lastErrorTime.stamp();
725 return true;
726}
727
729{
730 const auto pos = path.find(_aircraftDirectoryName);
731 return pos != std::string::npos;
732}
733
742{
743 const auto pos = path.find("Aircraft/");
744 if (pos == 0) {
745 // relative path which starts with "Aircraft/"
746 return true;
747 }
748
749 // TODO: decide if we also include paths which contain 'Aircraft' as a component
750 return false;
751}
752
753
755
757{
758 d->_logCallback.reset(new RecentLogCallback);
759
760 // define significant properties
761 d->_significantProperties = {
762 "/sim/aircraft-id",
763 "/sim/aircraft-dir",
764
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",
771
772 "/sim/rendering/preset-description",
773 "/sim/rendering/photoscenery/enabled",
774 "/sim/rendering/hdr/compute",
775
776 "/sim/rendering/max-paged-lod",
777 "/sim/rendering/multithreading-mode",
778 "/scenery/use-vpb",
779 };
780}
781
783{
784 // if we are deleted withut being shutdown(), ensure we clean
785 // up our logging callback
786 if (d->_logCallbackRegistered) {
787 sglog().removeCallback(d->_logCallback.get());
788 }
789}
790
792{
793 SGPropertyNode_ptr n = fgGetNode("/sim/error-report", true);
794
795 d->_enabledNode = n->getNode("enabled", true);
796 if (!d->_enabledNode->hasValue()) {
797 d->_enabledNode->setBoolValue(false); // default to off for now
798 }
799
800 d->_displayNode = n->getNode("display", true);
801 d->_activeErrorNode = n->getNode("active", true);
802 d->_mpReportNode = n->getNode("mp-report-enabled", true);
803}
804
806{
807 d->_enabledNode.clear();
808 d->_displayNode.clear();
809 d->_activeErrorNode.clear();
810}
811
813{
814 ErrorReporterPrivate* p = d.get();
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);
817 });
818
819 simgear::setErrorContextCallback([p](const std::string& key, const std::string& value) {
820 p->collectContext(key, value);
821 });
822
823 sglog().addCallback(d->_logCallback.get());
824 d->_logCallbackRegistered = true;
825}
826
828{
829 // we want to disable errors in developer mode, but since self-compiled
830 // builds default to developer-mode=true, need an override so people
831 // can see errors if they want
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;
835
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;
843 }
844 return;
845 }
846
847 globals->get_commands()->addCommand("dismiss-error-report", d.get(), &ErrorReporterPrivate::dismissReportCommand);
848 globals->get_commands()->addCommand("save-error-report-data", d.get(), &ErrorReporterPrivate::saveReportCommand);
849 globals->get_commands()->addCommand("show-error-report", d.get(), &ErrorReporterPrivate::showErrorReportCommand);
850
851 // cache these values here
852 d->_fgdataPathPrefix = globals->get_fg_root().utf8Str();
853 d->_terrasyncPathPrefix = globals->get_terrasync_dir().utf8Str();
854
855 const auto aircraftPath = SGPath::fromUtf8(fgGetString("/sim/aircraft-dir"));
856 d->_aircraftDirectoryName = aircraftPath.file();
857}
858
860{
861 bool showDialog = false;
862 bool showPopup = false;
863 bool havePendingReports = false;
864
865 // beginning of locked section
866 {
867 std::lock_guard<std::mutex> g(d->_lock);
868
869 if (!d->_enabledNode->getBoolValue()) {
870 return;
871 }
872
873 // we are into the update phase (postinit has ocurred). We treat errors
874 // after this point with lower severity, to avoid popups into a flight
875 d->_haveDonePostInit = true;
876
877 SGTimeStamp n = SGTimeStamp::now();
878
879 // ensure we pause between successive error dialogs
880 const auto timeSinceLastDialog = (n - d->_nextShowTimeout).toSecs();
881 if (timeSinceLastDialog < MinimumIntervalBetweenDialogs) {
882 return;
883 }
884
885 if (!d->_reportsDirty) {
886 return;
887 }
888
889 if (d->_activeReportIndex >= 0) {
890 return; // already showing a report
891 }
892
893 // check if any reports are due
894
895 // check if an error is current active
896 for (auto& report : d->_aggregated) {
897 if (report.type == Aggregation::MultiPlayer) {
898 if (!d->_mpReportNode->getBoolValue()) {
899 // mark it as shown, to supress it
900 report.haveShownToUser = true;
901 }
902 }
903
904 if (report.haveShownToUser) {
905 // unless we ever re-show?
906 continue;
907 }
908
909 const auto ageSec = (n - report.lastErrorTime).toSecs();
910 if (ageSec > NoNewErrorsTimeout) {
911 d->presentErrorToUser(report);
912 if (report.isCritical) {
913 showDialog = true;
914 } else {
915 showPopup = true;
916 }
917
918 d->sendReportToSentry(report);
919
920 // if we show one report, don't consider any others for now
921 break;
922 } else {
923 havePendingReports = true;
924 }
925 } // of active aggregates iteration
926
927 if (!havePendingReports) {
928 d->_reportsDirty = false;
929 }
930 } // end of locked section
931
933 showDialog = false;
934 showPopup = false;
935 }
936
937 // do not call into another subsystem with our lock held,
938 // as this can trigger deadlocks
939 if (showDialog) {
940 auto gui = globals->get_subsystem<NewGUI>();
941 gui->showDialog("error-report");
942 // this needs a bit more thought, disabling for the now
943#if 0
944 // pause the sim when showing the popup
945 SGPropertyNode_ptr pauseArgs(new SGPropertyNode);
946 pauseArgs->setBoolValue("force-pause", true);
947 globals->get_commands()->execute("do_pause", pauseArgs);
948#endif
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);
953 }
954}
955
957{
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");
962
963// during a reset we don't want to touch the log callback; it was added in
964// preinit, which does not get repeated on a reset
965 const bool inReset = fgGetBool("/sim/signals/reinit", false);
966 if (!inReset) {
967 sglog().removeCallback(d->_logCallback.get());
968 d->_logCallbackRegistered = false;
969 }
970 }
971}
972
973std::string ErrorReporter::threadSpecificContextValue(const std::string& key)
974{
975 auto it = thread_errorContextStack.find(key);
976 if (it == thread_errorContextStack.end())
977 return {};
978
979 return it->second.back();
980}
981
982
983} // namespace flightgear
984
985// Register the subsystem.
986SGSubsystemMgr::Registrant<flightgear::ErrorReporter> registrantErrorReporter(
987 SGSubsystemMgr::GENERAL);
SGSubsystemMgr::Registrant< flightgear::ErrorReporter > registrantErrorReporter(SGSubsystemMgr::GENERAL)
#define p(x)
static Ptr getInstance()
Get clipboard platform specific instance.
XML-configured GUI subsystem.
Definition new_gui.hxx:31
virtual bool showDialog(const std::string &name)
Display a dialog box.
Definition new_gui.cxx:281
AggregateErrors::iterator getAggregate(Aggregation ag, const std::string &param={})
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.
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
void writeSignificantPropertiesToStream(std::ostream &os) const
void writeLogToStream(const ErrorOcurrence &error, std::ostream &os) const
std::unique_ptr< RecentLogCallback > _logCallback
void collectError(simgear::LoadFailure type, simgear::ErrorCode code, const std::string &details, const sg_location &location)
void presentErrorToUser(AggregateReport &report)
std::vector< AggregateReport > AggregateErrors
void sendReportToSentry(AggregateReport &report)
void writeReportToStream(const AggregateReport &report, std::ostream &os) const
void update(double dt) override
static std::string threadSpecificContextValue(const std::string &key)
static Options * sharedInstance()
Definition options.cxx:2345
std::string fgGetString(const char *name, const char *defaultValue)
Get a string value for a property.
Definition fg_props.cxx:556
FGGlobals * globals
Definition globals.cxx:142
std::vector< std::string > string_list
Definition globals.hxx:36
FlightGear Localization Support.
const double g(9.80665)
FlightPlan.hxx - defines a full flight-plan object, including departure, cruise, arrival information ...
Definition Addon.cxx:53
void sentryReportUserError(const std::string &, const std::string &, const std::string &)
bool isHeadlessMode()
bool fgGetBool(char const *name, bool def)
Get a bool value for a property.
Definition proptest.cpp:25
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27
structure representing one or more errors, aggregated together
std::string parameter
base on type, the specific point. For example the add-on ID, AI model ident or custom scenery path
structure representing a single error which has occurred
std::string getContextValue(const std::string &key) const
void report(Airplane *a)