2021-02-09 16:56:12 +00:00
/*
* Copyright ( C ) 2021 James Turner
*
* This file is part of the program FlightGear .
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 2 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program . If not , see < http : //www.gnu.org/licenses/>.
*/
# include "config.h"
# include "ErrorReporter.hxx"
# include <algorithm>
2021-02-24 14:48:25 +00:00
# include <ctime> // for strftime, etc
2021-02-09 16:56:12 +00:00
# include <deque>
# include <map>
# include <mutex>
# include <simgear/debug/ErrorReportingCallback.hxx>
# include <simgear/debug/LogCallback.hxx>
# include <simgear/timing/timestamp.hxx>
# include <simgear/io/iostreams/sgstream.hxx>
# include <simgear/structure/commands.hxx>
2021-03-17 11:44:41 +00:00
# include <GUI/MessageBox.hxx>
2021-02-09 16:56:12 +00:00
# include <GUI/new_gui.hxx>
# include <Main/fg_props.hxx>
# include <Main/globals.hxx>
# include <Main/locale.hxx>
2021-03-15 14:08:36 +00:00
# include <Main/options.hxx>
2021-03-13 18:47:01 +00:00
# include <Main/sentryIntegration.hxx>
2021-02-09 16:56:12 +00:00
# include <Scripting/NasalClipboard.hxx> // clipboard access
using std : : string ;
namespace {
const double MinimumIntervalBetweenDialogs = 5.0 ;
const double NoNewErrorsTimeout = 8.0 ;
// map of context values; we allow a stack of values for
// cases such as sub-sub-model loading where we might process repeated
// nested model XML files
using PerThreadErrorContextStack = std : : map < std : : string , string_list > ;
// context storage. This is per-thread so parallel osgDB threads don't
// confuse each other
static thread_local PerThreadErrorContextStack thread_errorContextStack ;
/**
Define the aggregation points we use for errors , to direct the user towards the likely
source of problems .
*/
enum class Aggregation {
MainAircraft ,
HangarAircraft , // handle hangar aircraft differently
CustomScenery ,
TerraSync ,
AddOn ,
Scenario ,
InputDevice ,
FGData ,
2021-03-01 09:42:51 +00:00
MultiPlayer ,
2021-03-13 18:47:01 +00:00
Unknown , ///< error coudln't be attributed more specifcially
OutOfMemory , ///< seperate category to give it a custom message
2021-04-25 17:59:56 +00:00
Traffic ,
ShadersEffects
2021-02-09 16:56:12 +00:00
} ;
// these should correspond to simgear::ErrorCode enum
2021-04-25 17:59:56 +00:00
// they map to translateable strings in fgdata/Translations/sys.xml
2021-02-09 16:56:12 +00:00
string_list static_errorIds = {
" error-missing-shader " ,
" error-loading-texture " ,
" error-xml-model-load " ,
" error-3D-model-load " ,
" error-btg-load " ,
" error-scenario-load " ,
" error-dialog-load " ,
" error-audio-fx-load " ,
" error-xml-load-command " ,
" error-aircraft-systems " ,
2021-03-01 09:42:51 +00:00
" error-input-device-config " ,
2021-02-21 19:15:47 +00:00
" error-ai-traffic-schedule " ,
2021-03-01 09:42:51 +00:00
" error-terrasync " } ;
2021-02-09 16:56:12 +00:00
string_list static_errorTypeIds = {
" error-type-unknown " ,
" error-type-not-found " ,
" error-type-out-of-memory " ,
" error-type-bad-header " ,
" error-type-bad-data " ,
2021-03-01 09:42:51 +00:00
" error-type-misconfigured " ,
" error-type-io " ,
" error-type-network " } ;
2021-02-09 16:56:12 +00:00
string_list static_categoryIds = {
" error-category-aircraft " ,
" error-category-aircraft-from-hangar " ,
" error-category-custom-scenery " ,
" error-category-terrasync " ,
" error-category-addon " ,
" error-category-scenario " ,
" error-category-input-device " ,
" error-category-fgdata " ,
2021-03-01 09:42:51 +00:00
" error-category-multiplayer " ,
" error-category-unknown " ,
2021-03-13 18:47:01 +00:00
" error-category-out-of-memory " ,
2021-04-25 17:59:56 +00:00
" error-category-traffic " ,
" error-category-shaders " } ;
2021-02-09 16:56:12 +00:00
class RecentLogCallback : public simgear : : LogCallback
{
public :
RecentLogCallback ( ) : simgear : : LogCallback ( SG_ALL , SG_INFO )
{
}
bool doProcessEntry ( const simgear : : LogEntry & e ) override
{
ostringstream os ;
if ( e . file ! = nullptr ) {
os < < e . file < < " : " < < e . line < < " : \t " ;
}
os < < e . message ;
std : : lock_guard < std : : mutex > g ( _lock ) ; // begin access to shared state
_recentLogEntries . push_back ( os . str ( ) ) ;
while ( _recentLogEntries . size ( ) > _preceedingLogMessageCount ) {
2021-02-21 19:15:47 +00:00
_recentLogEntries . pop_front ( ) ;
2021-02-09 16:56:12 +00:00
}
return true ;
}
string_list copyRecentLogEntries ( ) const
{
std : : lock_guard < std : : mutex > g ( _lock ) ; // begin access to shared state
string_list r ( _recentLogEntries . begin ( ) , _recentLogEntries . end ( ) ) ;
return r ;
}
private :
mutable std : : mutex _lock ;
2021-02-21 21:49:58 +00:00
size_t _preceedingLogMessageCount = 6 ;
2021-02-09 16:56:12 +00:00
using LogDeque = std : : deque < string > ;
LogDeque _recentLogEntries ;
} ;
} // namespace
namespace flightgear {
class ErrorReporter : : ErrorReporterPrivate
{
public :
bool _reportsDirty = false ;
std : : mutex _lock ;
SGTimeStamp _nextShowTimeout ;
2021-04-30 12:46:03 +00:00
bool _haveDonePostInit = false ;
2021-02-09 16:56:12 +00:00
SGPropertyNode_ptr _enabledNode ;
SGPropertyNode_ptr _displayNode ;
SGPropertyNode_ptr _activeErrorNode ;
2021-03-01 09:42:51 +00:00
SGPropertyNode_ptr _mpReportNode ;
2021-02-09 16:56:12 +00:00
using ErrorContext = std : : map < std : : string , std : : string > ;
/**
strucutre representing a single error which has cocurred
*/
struct ErrorOcurrence {
simgear : : ErrorCode code ;
simgear : : LoadFailure type ;
string detailedInfo ;
sg_location origin ;
2021-02-24 14:48:25 +00:00
time_t when ;
2021-02-09 16:56:12 +00:00
string_list log ;
ErrorContext context ;
bool hasContextKey ( const std : : string & key ) const
{
return context . find ( key ) ! = context . end ( ) ;
}
std : : string getContextValue ( const std : : string & key ) const
{
auto it = context . find ( key ) ;
if ( it = = context . end ( ) )
return { } ;
return it - > second ;
}
} ;
using OccurrenceVec = std : : vector < ErrorOcurrence > ;
std : : unique_ptr < RecentLogCallback > _logCallback ;
2021-06-08 16:46:05 +00:00
bool _logCallbackRegistered = false ;
2021-02-09 16:56:12 +00:00
string _terrasyncPathPrefix ;
string _fgdataPathPrefix ;
/**
structure representing one or more errors , aggregated together
*/
struct AggregateReport {
Aggregation type ;
std : : string parameter ; ///< base on type, the specific point. For example the add-on ID, AI model ident or custom scenery path
SGTimeStamp lastErrorTime ;
bool haveShownToUser = false ;
OccurrenceVec errors ;
2021-04-25 17:59:56 +00:00
bool isCritical = false ;
2021-02-24 14:48:25 +00:00
bool addOccurence ( const ErrorOcurrence & err ) ;
2021-02-09 16:56:12 +00:00
} ;
using AggregateErrors = std : : vector < AggregateReport > ;
AggregateErrors _aggregated ;
2021-04-30 12:46:03 +00:00
int _activeReportIndex = - 1 ;
2021-04-02 09:48:39 +00:00
string_list _significantProperties ; ///< properties we want to include in reports, for debugging
2021-02-09 16:56:12 +00:00
/**
find the appropriate agrgegate for an error , based on its context
*/
AggregateErrors : : iterator getAggregateForOccurence ( const ErrorOcurrence & oc ) ;
AggregateErrors : : iterator getAggregate ( Aggregation ag , const std : : string & param = { } ) ;
void collectError ( simgear : : LoadFailure type , simgear : : ErrorCode code , const std : : string & details , const sg_location & location )
{
2021-02-24 14:48:25 +00:00
ErrorOcurrence occurrence { code , type , details , location , time ( nullptr ) } ;
2021-02-09 16:56:12 +00:00
// snapshot the top of the context stacks into our occurence data
for ( const auto & c : thread_errorContextStack ) {
occurrence . context [ c . first ] = c . second . back ( ) ;
}
occurrence . log = _logCallback - > copyRecentLogEntries ( ) ;
std : : lock_guard < std : : mutex > g ( _lock ) ; // begin access to shared state
auto it = getAggregateForOccurence ( occurrence ) ;
2021-02-24 14:48:25 +00:00
// add to the occurence, if it's not a duplicate
2021-04-25 17:59:56 +00:00
if ( ! it - > addOccurence ( occurrence ) ) {
return ; // duplicate, nothing else to do
}
// log it once we know it's not a duplicate
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 ( ) ) ;
it - > lastErrorTime . stamp ( ) ;
_reportsDirty = true ;
const auto ty = it - > type ;
// decide if it's a critical error or not
2021-04-30 12:46:03 +00:00
if ( ( ty = = Aggregation : : OutOfMemory ) | | ( ty = = Aggregation : : InputDevice ) ) {
it - > isCritical = true ;
}
// aircraft errors are critical if they occur during initial
// aircraft load, otherwise we just show the warning
if ( ! _haveDonePostInit & & ( ty = = Aggregation : : MainAircraft ) ) {
2021-04-25 17:59:56 +00:00
it - > isCritical = true ;
}
if ( code = = simgear : : ErrorCode : : LoadEffectsShaders ) {
2021-04-30 12:46:03 +00:00
it - > isCritical = true ;
2021-02-24 14:48:25 +00:00
}
2021-02-09 16:56:12 +00:00
}
void collectContext ( const std : : string & key , const std : : string & value )
{
if ( value = = " POP " ) {
auto it = thread_errorContextStack . find ( key ) ;
assert ( it ! = thread_errorContextStack . end ( ) ) ;
assert ( ! it - > second . empty ( ) ) ;
it - > second . pop_back ( ) ;
if ( it - > second . empty ( ) ) {
thread_errorContextStack . erase ( it ) ;
}
} else {
thread_errorContextStack [ key ] . push_back ( value ) ;
}
}
void presentErrorToUser ( AggregateReport & report )
{
const int catId = static_cast < int > ( report . type ) ;
auto catLabel = globals - > get_locale ( ) - > getLocalizedString ( static_categoryIds . at ( catId ) , " sys " ) ;
catLabel = simgear : : strutils : : replace ( catLabel , " %VALUE% " , report . parameter ) ;
_displayNode - > setStringValue ( " category " , catLabel ) ;
2021-02-25 22:02:32 +00:00
auto ns = globals - > get_locale ( ) - > getLocalizedString ( " error-next-steps " , " sys " ) ;
_displayNode - > setStringValue ( " next-steps " , ns ) ;
2021-02-09 16:56:12 +00:00
// remove any existing error children
_displayNode - > removeChildren ( " error " ) ;
ostringstream detailsTextStream ;
// add all the discrete errors as child nodes with all their information
for ( const auto & e : report . errors ) {
SGPropertyNode_ptr errNode = _displayNode - > addChild ( " error " ) ;
const auto em = globals - > get_locale ( ) - > getLocalizedString ( static_errorIds . at ( static_cast < int > ( e . code ) ) , " sys " ) ;
errNode - > setStringValue ( " message " , em ) ;
errNode - > setIntValue ( " code " , static_cast < int > ( e . code ) ) ;
const auto et = globals - > get_locale ( ) - > getLocalizedString ( static_errorTypeIds . at ( static_cast < int > ( e . type ) ) , " sys " ) ;
errNode - > setStringValue ( " type-message " , et ) ;
errNode - > setIntValue ( " type " , static_cast < int > ( e . type ) ) ;
errNode - > setStringValue ( " details " , e . detailedInfo ) ;
detailsTextStream < < em < < " : " < < et < < " \n " ;
detailsTextStream < < " ( " < < e . detailedInfo < < " ) \n " ;
if ( e . origin . isValid ( ) ) {
errNode - > setStringValue ( " location " , e . origin . asString ( ) ) ;
detailsTextStream < < " from: " < < e . origin . asString ( ) < < " \n " ;
}
detailsTextStream < < " \n " ;
} // of errors within the report iteration
_displayNode - > setStringValue ( " details-text " , detailsTextStream . str ( ) ) ;
_activeErrorNode - > setBoolValue ( true ) ;
report . haveShownToUser = true ;
// compute index; slightly clunky, find the report in _aggregated
auto it = std : : find_if ( _aggregated . begin ( ) , _aggregated . end ( ) , [ report ] ( const AggregateReport & a ) {
if ( a . type ! = report . type ) return false ;
return report . parameter . empty ( ) ? true : report . parameter = = a . parameter ;
} ) ;
assert ( it ! = _aggregated . end ( ) ) ;
2021-04-30 12:46:03 +00:00
_activeReportIndex = static_cast < int > ( std : : distance ( _aggregated . begin ( ) , it ) ) ;
_displayNode - > setBoolValue ( " index " , _activeReportIndex ) ;
2021-04-25 17:59:56 +00:00
_displayNode - > setBoolValue ( " have-next " , _activeReportIndex < ( _aggregated . size ( ) - 1 ) ) ;
2021-04-30 12:46:03 +00:00
_displayNode - > setBoolValue ( " have-previous " , _activeReportIndex > 0 ) ;
2021-04-25 17:59:56 +00:00
}
2021-03-13 18:47:01 +00:00
2021-04-25 17:59:56 +00:00
void sendReportToSentry ( AggregateReport & report )
{
const int catId = static_cast < int > ( report . type ) ;
flightgear : : sentryReportUserError ( static_categoryIds . at ( catId ) , _displayNode - > getStringValue ( ) ) ;
2021-02-09 16:56:12 +00:00
}
void writeReportToStream ( const AggregateReport & report , std : : ostream & os ) const ;
void writeContextToStream ( const ErrorOcurrence & error , std : : ostream & os ) const ;
void writeLogToStream ( const ErrorOcurrence & error , std : : ostream & os ) const ;
2021-04-02 09:48:39 +00:00
void writeSignificantPropertiesToStream ( std : : ostream & os ) const ;
2021-02-09 16:56:12 +00:00
bool dismissReportCommand ( const SGPropertyNode * args , SGPropertyNode * ) ;
bool saveReportCommand ( const SGPropertyNode * args , SGPropertyNode * ) ;
2021-04-25 17:59:56 +00:00
bool showErrorReportCommand ( const SGPropertyNode * args , SGPropertyNode * ) ;
2021-02-09 16:56:12 +00:00
} ;
auto ErrorReporter : : ErrorReporterPrivate : : getAggregateForOccurence ( const ErrorReporter : : ErrorReporterPrivate : : ErrorOcurrence & oc )
- > AggregateErrors : : iterator
{
2021-03-01 09:42:51 +00:00
// all OOM errors go to a dedicated category. This is so we don't blame
// out of memory on the aircraft/scenery/etc, when it's not the underlying
// cause.
if ( oc . type = = simgear : : LoadFailure : : OutOfMemory ) {
return getAggregate ( Aggregation : : OutOfMemory , { } ) ;
}
2021-02-09 16:56:12 +00:00
if ( oc . hasContextKey ( " primary-aircraft " ) ) {
const auto fullId = fgGetString ( " /sim/aircraft-id " ) ;
if ( fullId ! = fgGetString ( " /sim/aircraft " ) ) {
2021-03-15 14:08:36 +00:00
return getAggregate ( Aggregation : : MainAircraft , fullId ) ;
2021-02-09 16:56:12 +00:00
}
return getAggregate ( Aggregation : : HangarAircraft , fullId ) ;
}
2021-03-01 09:42:51 +00:00
if ( oc . hasContextKey ( " multiplayer " ) ) {
return getAggregate ( Aggregation : : MultiPlayer , { } ) ;
}
2021-03-13 18:47:01 +00:00
// traffic cases: need to handle errors in the traffic files (schedule, rwyuse)
// but also errors loading aircraft models associated with traffic
if ( oc . code = = simgear : : ErrorCode : : AITrafficSchedule ) {
return getAggregate ( Aggregation : : Traffic , { } ) ;
}
if ( oc . hasContextKey ( " traffic-aircraft-callsign " ) ) {
return getAggregate ( Aggregation : : Traffic , { } ) ;
}
2021-03-01 09:42:51 +00:00
// all TerraSync coded errors go there: this is errors for the
// actual download process (eg, failed to write to disk)
if ( oc . code = = simgear : : ErrorCode : : TerraSync ) {
return getAggregate ( Aggregation : : TerraSync , { } ) ;
}
2021-02-25 22:02:32 +00:00
if ( oc . hasContextKey ( " terrain-stg " ) ) {
2021-02-09 16:56:12 +00:00
// determine if it's custom scenery, TerraSync or FGData
// bucket is no use here, we need to check the BTG/XML/STG path etc.
// STG is probably the best bet. This ensures if a custom scenery
// STG references a model, XML or texture in FGData or TerraSync
// incorrectly, we attribute the error to the scenery, which is
// likely what we want/expect
2021-02-25 22:02:32 +00:00
const auto stgPath = oc . getContextValue ( " terrain-stg " ) ;
2021-02-09 16:56:12 +00:00
if ( simgear : : strutils : : starts_with ( stgPath , _fgdataPathPrefix ) ) {
return getAggregate ( Aggregation : : FGData , { } ) ;
} else if ( simgear : : strutils : : starts_with ( stgPath , _terrasyncPathPrefix ) ) {
return getAggregate ( Aggregation : : TerraSync , { } ) ;
}
// custom scenery, find out the prefix
for ( const auto & sceneryPath : globals - > get_fg_scenery ( ) ) {
const auto pathStr = sceneryPath . utf8Str ( ) ;
if ( simgear : : strutils : : starts_with ( stgPath , pathStr ) ) {
return getAggregate ( Aggregation : : CustomScenery , pathStr ) ;
}
}
// shouldn't ever happen
return getAggregate ( Aggregation : : CustomScenery , { } ) ;
}
2021-02-25 22:02:32 +00:00
if ( oc . hasContextKey ( " scenario-path " ) ) {
const auto scenarioPath = oc . getContextValue ( " scenario-path " ) ;
return getAggregate ( Aggregation : : Scenario , scenarioPath ) ;
2021-02-09 16:56:12 +00:00
}
if ( oc . hasContextKey ( " input-device " ) ) {
return getAggregate ( Aggregation : : InputDevice , oc . getContextValue ( " input-device " ) ) ;
}
// GUI dialog errors often have no context
if ( oc . code = = simgear : : ErrorCode : : GUIDialog ) {
// TODO: check if it's an aircraft dialog and use MainAicraft
// check if it's an add-on and use that
return getAggregate ( Aggregation : : FGData ) ;
}
2021-04-25 17:59:56 +00:00
// becuase we report shader errors from the main thread, they don't
// get attributed. Collect them into their own category, which also
// means we can display a more specific message
if ( oc . code = = simgear : : ErrorCode : : LoadEffectsShaders ) {
return getAggregate ( Aggregation : : ShadersEffects ) ;
}
2021-02-09 16:56:12 +00:00
return getAggregate ( Aggregation : : Unknown ) ;
}
auto ErrorReporter : : ErrorReporterPrivate : : getAggregate ( Aggregation ag , const std : : string & param )
- > AggregateErrors : : iterator
{
auto it = std : : find_if ( _aggregated . begin ( ) , _aggregated . end ( ) , [ ag , & param ] ( const AggregateReport & a ) {
if ( a . type ! = ag ) return false ;
return param . empty ( ) ? true : param = = a . parameter ;
} ) ;
if ( it = = _aggregated . end ( ) ) {
AggregateReport r ;
r . type = ag ;
r . parameter = param ;
_aggregated . push_back ( r ) ;
it = _aggregated . end ( ) - 1 ;
}
return it ;
}
void ErrorReporter : : ErrorReporterPrivate : : writeReportToStream ( const AggregateReport & report , std : : ostream & os ) const
{
2021-02-24 14:48:25 +00:00
os < < " FlightGear " < < VERSION < < " error report, created at " ;
{
char buf [ 64 ] ;
time_t now = time ( nullptr ) ;
strftime ( buf , sizeof ( buf ) , " %a, %d %b %Y %H:%M:%S GMT " , gmtime ( & now ) ) ;
os < < buf < < endl ;
}
2021-02-09 16:56:12 +00:00
os < < " Category: " < < static_categoryIds . at ( static_cast < int > ( report . type ) ) < < endl ;
if ( ! report . parameter . empty ( ) ) {
2021-02-24 14:48:25 +00:00
os < < " \t Parameter: " < < report . parameter < < endl ;
2021-02-09 16:56:12 +00:00
}
os < < endl ; // insert a blank line after header data
int index = 1 ;
2021-02-24 14:48:25 +00:00
char whenBuf [ 64 ] ;
2021-02-09 16:56:12 +00:00
for ( const auto & err : report . errors ) {
os < < " Error " < < index + + < < std : : endl ;
os < < " \t code: " < < static_errorIds . at ( static_cast < int > ( err . code ) ) < < endl ;
os < < " \t type: " < < static_errorTypeIds . at ( static_cast < int > ( err . type ) ) < < endl ;
2021-02-24 14:48:25 +00:00
strftime ( whenBuf , sizeof ( whenBuf ) , " %H:%M:%S GMT " , gmtime ( & err . when ) ) ;
os < < " \t when: " < < whenBuf < < endl ;
2021-02-09 16:56:12 +00:00
os < < " \t " < < err . detailedInfo < < std : : endl ;
os < < " \t location: " < < err . origin . asString ( ) < < endl ;
writeContextToStream ( err , os ) ;
writeLogToStream ( err , os ) ;
os < < std : : endl ; // trailing blank line
}
2021-03-15 14:08:36 +00:00
os < < " Command line / launcher / fgfsrc options " < < endl ;
for ( auto o : Options : : sharedInstance ( ) - > extractOptions ( ) ) {
os < < " \t " < < o < < " \n " ;
}
os < < endl ;
2021-04-02 09:48:39 +00:00
writeSignificantPropertiesToStream ( os ) ;
2021-02-09 16:56:12 +00:00
}
2021-04-02 09:48:39 +00:00
void ErrorReporter : : ErrorReporterPrivate : : writeSignificantPropertiesToStream ( std : : ostream & os ) const
{
os < < " Properties: " < < endl ;
for ( const auto & ps : _significantProperties ) {
auto node = fgGetNode ( ps ) ;
if ( ! node ) {
os < < " \t " < < ps < < " : not defined \n " ;
} else {
os < < " \t " < < ps < < " : " < < node - > getStringValue ( ) < < " \n " ;
}
}
os < < endl ;
}
2021-02-09 16:56:12 +00:00
bool ErrorReporter : : ErrorReporterPrivate : : dismissReportCommand ( const SGPropertyNode * args , SGPropertyNode * )
{
std : : lock_guard < std : : mutex > g ( _lock ) ;
_activeErrorNode - > setBoolValue ( false ) ;
if ( args - > getBoolValue ( " dont-show " ) ) {
// TODO implement dont-show behaviour
}
// clear any values underneath displayNode?
_nextShowTimeout . stamp ( ) ;
_reportsDirty = true ; // set this so we check for another report to present
_activeReportIndex = - 1 ;
return true ;
}
2021-04-25 17:59:56 +00:00
bool ErrorReporter : : ErrorReporterPrivate : : showErrorReportCommand ( const SGPropertyNode * args , SGPropertyNode * )
{
std : : lock_guard < std : : mutex > g ( _lock ) ;
if ( _aggregated . empty ( ) ) {
return false ;
}
2021-04-30 12:46:03 +00:00
const auto numAggregates = static_cast < int > ( _aggregated . size ( ) ) ;
2021-04-25 17:59:56 +00:00
if ( args - > getBoolValue ( " next " ) ) {
_activeReportIndex + + ;
2021-04-30 12:46:03 +00:00
if ( _activeReportIndex > = numAggregates ) {
2021-04-25 17:59:56 +00:00
return false ;
}
2021-04-30 12:46:03 +00:00
} else if ( args - > getBoolValue ( " previous " ) ) {
if ( _activeReportIndex < 1 ) {
return false ;
}
_activeReportIndex - - ;
2021-04-25 17:59:56 +00:00
} else if ( args - > hasChild ( " index " ) ) {
_activeReportIndex = args - > getIntValue ( " index " ) ;
2021-04-30 12:46:03 +00:00
if ( ( _activeReportIndex < 0 ) | | ( _activeReportIndex > = numAggregates ) ) {
2021-04-25 17:59:56 +00:00
return false ;
}
} else {
_activeReportIndex = 0 ;
}
auto & report = _aggregated . at ( _activeReportIndex ) ;
presentErrorToUser ( report ) ;
auto gui = globals - > get_subsystem < NewGUI > ( ) ;
if ( ! gui - > getDialog ( " error-report " ) ) {
gui - > showDialog ( " error-report " ) ;
}
return true ;
}
2021-02-09 16:56:12 +00:00
bool ErrorReporter : : ErrorReporterPrivate : : saveReportCommand ( const SGPropertyNode * args , SGPropertyNode * )
{
if ( _activeReportIndex < 0 ) {
return false ;
}
const auto & report = _aggregated . at ( _activeReportIndex ) ;
const string where = args - > getStringValue ( " where " ) ;
2021-02-24 14:48:25 +00:00
string when ;
{
char buf [ 64 ] ;
time_t now = time ( nullptr ) ;
strftime ( buf , sizeof ( buf ) , " %Y%m%d " , gmtime ( & now ) ) ;
when = buf ;
}
2021-02-09 16:56:12 +00:00
if ( where . empty ( ) | | ( where = = " !desktop " ) ) {
2021-02-24 14:48:25 +00:00
SGPath p = SGPath : : desktop ( ) / ( " flightgear_error_ " + when + " .txt " ) ;
2021-02-09 16:56:12 +00:00
int uniqueCount = 2 ;
while ( p . exists ( ) ) {
2021-02-24 14:48:25 +00:00
p = SGPath : : desktop ( ) / ( " flightgear_error_ " + when + " _ " + std : : to_string ( uniqueCount + + ) + " .txt " ) ;
2021-02-09 16:56:12 +00:00
}
sg_ofstream f ( p , std : : ios_base : : out ) ;
writeReportToStream ( report , f ) ;
} else if ( where = = " !clipboard " ) {
std : : ostringstream os ;
writeReportToStream ( report , os ) ;
NasalClipboard : : getInstance ( ) - > setText ( os . str ( ) ) ;
}
return true ;
}
void ErrorReporter : : ErrorReporterPrivate : : writeContextToStream ( const ErrorOcurrence & error , std : : ostream & os ) const
{
os < < " \t context: \n " ;
for ( const auto & c : error . context ) {
os < < " \t \t " < < c . first < < " = " < < c . second < < " \n " ;
}
}
void ErrorReporter : : ErrorReporterPrivate : : writeLogToStream ( const ErrorOcurrence & error , std : : ostream & os ) const
{
os < < " \t preceeding log: \n " ;
for ( const auto & c : error . log ) {
os < < " \t \t " < < c < < " \n " ;
}
}
2021-02-24 14:48:25 +00:00
bool ErrorReporter : : ErrorReporterPrivate : : AggregateReport : : addOccurence ( const ErrorOcurrence & err )
{
auto it = std : : find_if ( errors . begin ( ) , errors . end ( ) , [ err ] ( const ErrorOcurrence & ext ) {
// check if the two occurences match for the purposes of
// de-duplication.
return ( ext . code = = err . code ) & &
( ext . type = = err . type ) & &
( ext . detailedInfo = = err . detailedInfo ) & &
( ext . origin . asString ( ) = = err . origin . asString ( ) ) ;
} ) ;
if ( it ! = errors . end ( ) ) {
return false ; // duplicate, don't add
}
errors . push_back ( err ) ;
lastErrorTime . stamp ( ) ;
return true ;
}
2021-02-09 16:56:12 +00:00
////////////////////////////////////////////
ErrorReporter : : ErrorReporter ( ) : d ( new ErrorReporterPrivate )
{
d - > _logCallback . reset ( new RecentLogCallback ) ;
2021-04-02 09:48:39 +00:00
// define significant properties
d - > _significantProperties = {
" /sim/aircraft-id " ,
" /sim/aircraft-dir " ,
" /sim/rendering/gl-version " ,
" /sim/rendering/gl-renderer " ,
" /sim/rendering/gl-shading-language-version " ,
" /sim/rendering/max-texture-size " ,
" /sim/rendering/max-texture-units " ,
" /sim/rendering/shaders/skydome " ,
" /sim/rendering/shaders/water " ,
" /sim/rendering/shaders/model " ,
" /sim/rendering/shaders/landmass " ,
" /sim/rendering/shaders/vegetation-effects " ,
" /sim/rendering/shaders/transition " ,
" /sim/rendering/max-paged-lod " ,
" /sim/rendering/photoscenery/enabled " ,
" /sim/rendering/preset-description " ,
" /sim/rendering/multithreading-mode " ,
" /sim/rendering/multi-sample-buffers " ,
" /sim/rendering/multi-samples " ,
" /scenery/use-vpb " } ;
2021-02-09 16:56:12 +00:00
}
2021-06-08 16:46:05 +00:00
ErrorReporter : : ~ ErrorReporter ( )
{
// if we are deleted withut being shutdown(), ensure we clean
// up our logging callback
if ( d - > _logCallbackRegistered ) {
sglog ( ) . removeCallback ( d - > _logCallback . get ( ) ) ;
}
}
2021-02-09 16:56:12 +00:00
void ErrorReporter : : bind ( )
{
SGPropertyNode_ptr n = fgGetNode ( " /sim/error-report " , true ) ;
d - > _enabledNode = n - > getNode ( " enabled " , true ) ;
2021-02-21 19:15:47 +00:00
if ( ! d - > _enabledNode - > hasValue ( ) ) {
d - > _enabledNode - > setBoolValue ( false ) ; // default to off for now
}
2021-02-09 16:56:12 +00:00
d - > _displayNode = n - > getNode ( " display " , true ) ;
d - > _activeErrorNode = n - > getNode ( " active " , true ) ;
2021-03-01 09:42:51 +00:00
d - > _mpReportNode = n - > getNode ( " mp-report-enabled " , true ) ;
2021-02-09 16:56:12 +00:00
}
void ErrorReporter : : unbind ( )
{
d - > _enabledNode . clear ( ) ;
d - > _displayNode . clear ( ) ;
d - > _activeErrorNode . clear ( ) ;
}
void ErrorReporter : : preinit ( )
{
ErrorReporterPrivate * p = d . get ( ) ;
simgear : : setFailureCallback ( [ p ] ( simgear : : LoadFailure type , simgear : : ErrorCode code , const std : : string & details , const sg_location & location ) {
p - > collectError ( type , code , details , location ) ;
} ) ;
simgear : : setErrorContextCallback ( [ p ] ( const std : : string & key , const std : : string & value ) {
p - > collectContext ( key , value ) ;
} ) ;
sglog ( ) . addCallback ( d - > _logCallback . get ( ) ) ;
2021-06-08 16:46:05 +00:00
d - > _logCallbackRegistered = true ;
2021-02-09 16:56:12 +00:00
}
void ErrorReporter : : init ( )
{
2021-04-30 12:46:03 +00:00
// we want to disable errors in developer mode, but since self-compiled
// builds default to developer-mode=true, need an override so people
// can see errors if they want
const auto developerMode = fgGetBool ( " sim/developer-mode " ) ;
const auto disableInDeveloperMode = ! d - > _enabledNode - > getParent ( ) - > getBoolValue ( " enable-in-developer-mode " ) ;
const auto dd = developerMode & & disableInDeveloperMode ;
if ( dd | | ! d - > _enabledNode ) {
2021-02-21 19:15:47 +00:00
SG_LOG ( SG_GENERAL , SG_INFO , " Error reporting disabled " ) ;
simgear : : setFailureCallback ( simgear : : FailureCallback ( ) ) ;
simgear : : setErrorContextCallback ( simgear : : ContextCallback ( ) ) ;
2021-07-26 10:48:06 +00:00
if ( d - > _logCallbackRegistered ) {
sglog ( ) . removeCallback ( d - > _logCallback . get ( ) ) ;
d - > _logCallbackRegistered = false ;
}
2021-02-21 19:15:47 +00:00
return ;
}
2021-02-09 16:56:12 +00:00
globals - > get_commands ( ) - > addCommand ( " dismiss-error-report " , d . get ( ) , & ErrorReporterPrivate : : dismissReportCommand ) ;
globals - > get_commands ( ) - > addCommand ( " save-error-report-data " , d . get ( ) , & ErrorReporterPrivate : : saveReportCommand ) ;
2021-04-25 17:59:56 +00:00
globals - > get_commands ( ) - > addCommand ( " show-error-report " , d . get ( ) , & ErrorReporterPrivate : : showErrorReportCommand ) ;
2021-02-09 16:56:12 +00:00
// cache these values here
d - > _fgdataPathPrefix = globals - > get_fg_root ( ) . utf8Str ( ) ;
d - > _terrasyncPathPrefix = globals - > get_terrasync_dir ( ) . utf8Str ( ) ;
}
void ErrorReporter : : update ( double dt )
{
bool showDialog = false ;
2021-04-25 17:59:56 +00:00
bool showPopup = false ;
2021-03-01 09:42:51 +00:00
bool havePendingReports = false ;
2021-02-09 16:56:12 +00:00
// beginning of locked section
{
std : : lock_guard < std : : mutex > g ( d - > _lock ) ;
2021-02-21 19:15:47 +00:00
if ( ! d - > _enabledNode - > getBoolValue ( ) ) {
return ;
}
2021-04-30 12:46:03 +00:00
// we are into the update phase (postinit has ocurred). We treat errors
// after this point with lower severity, to avoid popups into a flight
d - > _haveDonePostInit = true ;
2021-02-09 16:56:12 +00:00
SGTimeStamp n = SGTimeStamp : : now ( ) ;
// ensure we pause between successive error dialogs
const auto timeSinceLastDialog = ( n - d - > _nextShowTimeout ) . toSecs ( ) ;
if ( timeSinceLastDialog < MinimumIntervalBetweenDialogs ) {
return ;
}
if ( ! d - > _reportsDirty ) {
return ;
}
if ( d - > _activeReportIndex > = 0 ) {
return ; // already showing a report
}
// check if any reports are due
// check if an error is current active
for ( auto & report : d - > _aggregated ) {
2021-03-01 09:42:51 +00:00
if ( report . type = = Aggregation : : MultiPlayer ) {
if ( ! d - > _mpReportNode - > getBoolValue ( ) ) {
// mark it as shown, to supress it
report . haveShownToUser = true ;
}
}
2021-02-09 16:56:12 +00:00
if ( report . haveShownToUser ) {
// unless we ever re-show?
continue ;
}
const auto ageSec = ( n - report . lastErrorTime ) . toSecs ( ) ;
if ( ageSec > NoNewErrorsTimeout ) {
d - > presentErrorToUser ( report ) ;
2021-04-25 17:59:56 +00:00
if ( report . isCritical ) {
showDialog = true ;
} else {
showPopup = true ;
}
d - > sendReportToSentry ( report ) ;
2021-02-09 16:56:12 +00:00
// if we show one report, don't consider any others for now
break ;
2021-03-01 09:42:51 +00:00
} else {
havePendingReports = true ;
2021-02-09 16:56:12 +00:00
}
} // of active aggregates iteration
2021-03-01 09:42:51 +00:00
if ( ! havePendingReports ) {
d - > _reportsDirty = false ;
}
2021-02-09 16:56:12 +00:00
} // end of locked section
2021-04-25 17:59:56 +00:00
if ( flightgear : : isHeadlessMode ( ) ) {
2021-03-17 11:44:41 +00:00
showDialog = false ;
2021-04-25 17:59:56 +00:00
showPopup = false ;
2021-03-17 11:44:41 +00:00
}
2021-02-09 16:56:12 +00:00
// do not call into another subsystem with our lock held,
// as this can trigger deadlocks
if ( showDialog ) {
auto gui = globals - > get_subsystem < NewGUI > ( ) ;
gui - > showDialog ( " error-report " ) ;
2021-03-15 14:08:36 +00:00
// this needs a bit more thought, disabling for the now
#if 0
// pause the sim when showing the popup
SGPropertyNode_ptr pauseArgs ( new SGPropertyNode ) ;
pauseArgs - > setBoolValue ( " force-pause " , true ) ;
globals - > get_commands ( ) - > execute ( " do_pause " , pauseArgs ) ;
# endif
2021-04-25 17:59:56 +00:00
} else if ( showPopup ) {
SGPropertyNode_ptr popupArgs ( new SGPropertyNode ) ;
2021-04-30 12:46:03 +00:00
popupArgs - > setIntValue ( " index " , d - > _activeReportIndex ) ;
2021-04-25 17:59:56 +00:00
globals - > get_commands ( ) - > execute ( " show-error-notification-popup " , popupArgs , nullptr ) ;
2021-02-09 16:56:12 +00:00
}
}
void ErrorReporter : : shutdown ( )
{
2021-02-21 19:15:47 +00:00
if ( d - > _enabledNode ) {
globals - > get_commands ( ) - > removeCommand ( " dismiss-error-report " ) ;
globals - > get_commands ( ) - > removeCommand ( " save-error-report-data " ) ;
2021-04-25 17:59:56 +00:00
globals - > get_commands ( ) - > removeCommand ( " show-error-report " ) ;
2021-07-26 10:48:06 +00:00
// during a reset we don't want to touch the log callback; it was added in
// preinit, which does not get repeated on a reset
const bool inReset = fgGetBool ( " /sim/signals/reinit " , false ) ;
if ( ! inReset ) {
sglog ( ) . removeCallback ( d - > _logCallback . get ( ) ) ;
d - > _logCallbackRegistered = false ;
}
2021-02-21 19:15:47 +00:00
}
2021-02-09 16:56:12 +00:00
}
2021-02-25 22:02:32 +00:00
std : : string ErrorReporter : : threadSpecificContextValue ( const std : : string & key )
{
auto it = thread_errorContextStack . find ( key ) ;
if ( it = = thread_errorContextStack . end ( ) )
return { } ;
return it - > second . back ( ) ;
}
2021-02-09 16:56:12 +00:00
} // namespace flightgear