feat(GODT-2364): added optional details to C++ exceptions.

This commit is contained in:
Xavier Michelon
2023-02-15 15:55:23 +01:00
committed by Jakub
parent 10cf153678
commit 9ad5f74409
12 changed files with 75 additions and 48 deletions

View File

@ -73,13 +73,17 @@ ProcessMonitor *AppController::bridgeMonitor() const {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] function The function that caught the exception. /// \param[in] function The function that caught the exception.
/// \param[in] message The error message. /// \param[in] message The error message.
/// \param[in] details The details for the error.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void AppController::onFatalError(QString const &function, QString const &message) { void AppController::onFatalError(QString const &function, QString const &message, QString const& details) {
QString const fullMessage = QString("%1(): %2").arg(function, message); QString fullMessage = QString("%1(): %2").arg(function, message);
auto uuid = reportSentryException(SENTRY_LEVEL_ERROR, "AppController got notified of a fatal error", "Exception", fullMessage.toLocal8Bit()); if (!details.isEmpty())
fullMessage += "\n\nDetails:\n" + details;
sentry_uuid_s const uuid = reportSentryException(SENTRY_LEVEL_ERROR, "AppController got notified of a fatal error", "Exception",
fullMessage.toLocal8Bit());
QMessageBox::critical(nullptr, tr("Error"), message); QMessageBox::critical(nullptr, tr("Error"), message);
restart(true); restart(true);
log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex()).arg(fullMessage)); log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), fullMessage));
qApp->exit(EXIT_FAILURE); qApp->exit(EXIT_FAILURE);
} }

View File

@ -58,7 +58,7 @@ public: // member functions.
void setLauncherArgs(const QString& launcher, const QStringList& args); void setLauncherArgs(const QString& launcher, const QStringList& args);
public slots: public slots:
void onFatalError(QString const &function, QString const &message); ///< Handle fatal errors. void onFatalError(QString const &function, QString const &message, QString const& details); ///< Handle fatal errors.
private: // member functions private: // member functions
AppController(); ///< Default constructor. AppController(); ///< Default constructor.

View File

@ -25,8 +25,8 @@
#define HANDLE_EXCEPTION(x) try { x } \ #define HANDLE_EXCEPTION(x) try { x } \
catch (Exception const &e) { emit fatalError(__func__, e.qwhat()); } \ catch (Exception const &e) { emit fatalError(__func__, e.qwhat(), e.details()); } \
catch (...) { emit fatalError(__func__, QString("An unknown exception occurred")); } catch (...) { emit fatalError(__func__, QString("An unknown exception occurred"), QString()); }
#define HANDLE_EXCEPTION_RETURN_BOOL(x) HANDLE_EXCEPTION(x) return false; #define HANDLE_EXCEPTION_RETURN_BOOL(x) HANDLE_EXCEPTION(x) return false;
#define HANDLE_EXCEPTION_RETURN_QSTRING(x) HANDLE_EXCEPTION(x) return QString(); #define HANDLE_EXCEPTION_RETURN_QSTRING(x) HANDLE_EXCEPTION(x) return QString();
#define HANDLE_EXCEPTION_RETURN_ZERO(x) HANDLE_EXCEPTION(x) return 0; #define HANDLE_EXCEPTION_RETURN_ZERO(x) HANDLE_EXCEPTION(x) return 0;
@ -56,12 +56,8 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
app().grpc().setLog(&log); app().grpc().setLog(&log);
this->connectGrpcEvents(); this->connectGrpcEvents();
QString error; app().grpc().connectToServer(serviceConfig, app().bridgeMonitor());
if (app().grpc().connectToServer(serviceConfig, app().bridgeMonitor(), error)) { app().log().info("Connected to backend via gRPC service.");
app().log().info("Connected to backend via gRPC service.");
} else {
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
}
QString bridgeVer; QString bridgeVer;
app().grpc().version(bridgeVer); app().grpc().version(bridgeVer);
@ -597,7 +593,8 @@ void QMLBackend::setDiskCachePath(QUrl const &path) const {
void QMLBackend::login(QString const &username, QString const &password) const { void QMLBackend::login(QString const &username, QString const &password) const {
HANDLE_EXCEPTION( HANDLE_EXCEPTION(
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) { if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot"); throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
"This error exists for test purposes and should be ignored.");
} }
app().grpc().login(username, password); app().grpc().login(username, password);
) )

View File

@ -235,7 +235,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void selectUser(QString const); ///< Signal that request the given user account to be displayed. void selectUser(QString const); ///< Signal that request the given user account to be displayed.
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise. // This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
void fatalError(QString const &function, QString const &message) const; ///< Signal emitted when an fatal error occurs. void fatalError(QString const &function, QString const &message, QString const &details) const; ///< Signal emitted when an fatal error occurs.
private: // member functions private: // member functions
void retrieveUserList(); ///< Retrieve the list of users via gRPC. void retrieveUserList(); ///< Retrieve the list of users via gRPC.

View File

@ -428,9 +428,16 @@ int main(int argc, char *argv[]) {
return result; return result;
} }
catch (Exception const &e) { catch (Exception const &e) {
auto uuid = reportSentryException(SENTRY_LEVEL_ERROR, "Exception occurred during main", "Exception", e.what()); QString fullMessage = e.qwhat();
bool const hasDetails = !e.details().isEmpty();
if (hasDetails)
fullMessage += "\n\nDetails:\n" + e.details();
sentry_uuid_s const uuid = reportSentryException(SENTRY_LEVEL_ERROR, "Exception occurred during main", "Exception", fullMessage.toLocal8Bit());
QMessageBox::critical(nullptr, "Error", e.qwhat()); QMessageBox::critical(nullptr, "Error", e.qwhat());
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << "Captured exception :" << e.qwhat() << "\n"; QTextStream errStream(stderr);
errStream << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.qwhat() << "\n";
if (hasDetails)
errStream << "\nDetails:\n" << e.details() << "\n";
return EXIT_FAILURE; return EXIT_FAILURE;
} }
} }

View File

@ -23,11 +23,13 @@ namespace bridgepp {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] what A description of the exception /// \param[in] what A description of the exception.
/// \param[in] details The optional details for the exception.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
Exception::Exception(QString what) noexcept Exception::Exception(QString what, QString details) noexcept
: std::exception() : std::exception()
, what_(std::move(what)) { , what_(std::move(what))
, details_(std::move(details)) {
} }
@ -36,7 +38,8 @@ Exception::Exception(QString what) noexcept
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
Exception::Exception(Exception const &ref) noexcept Exception::Exception(Exception const &ref) noexcept
: std::exception(ref) : std::exception(ref)
, what_(ref.what_) { , what_(ref.what_)
, details_(ref.details_) {
} }
@ -45,14 +48,15 @@ Exception::Exception(Exception const &ref) noexcept
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
Exception::Exception(Exception &&ref) noexcept Exception::Exception(Exception &&ref) noexcept
: std::exception(ref) : std::exception(ref)
, what_(ref.what_) { , what_(ref.what_)
, details_(ref.details_) {
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return a string describing the exception /// \return a string describing the exception
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
QString const &Exception::qwhat() const noexcept { QString Exception::qwhat() const noexcept {
return what_; return what_;
} }
@ -65,4 +69,12 @@ const char *Exception::what() const noexcept {
} }
//****************************************************************************************************************************************************
/// \return The details for the exception.
//****************************************************************************************************************************************************
QString Exception::details() const noexcept {
return details_;
}
} // namespace bridgepp } // namespace bridgepp

View File

@ -31,17 +31,19 @@ namespace bridgepp {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
class Exception : public std::exception { class Exception : public std::exception {
public: // member functions public: // member functions
explicit Exception(QString what = QString()) noexcept; ///< Constructor explicit Exception(QString what = QString(), QString details = QString()) noexcept; ///< Constructor
Exception(Exception const &ref) noexcept; ///< copy constructor Exception(Exception const &ref) noexcept; ///< copy constructor
Exception(Exception &&ref) noexcept; ///< copy constructor Exception(Exception &&ref) noexcept; ///< copy constructor
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator
Exception &operator=(Exception &&) = delete; ///< Disabled assignment operator Exception &operator=(Exception &&) = delete; ///< Disabled assignment operator
~Exception() noexcept override = default; ///< Destructor ~Exception() noexcept override = default; ///< Destructor
QString const &qwhat() const noexcept; ///< Return the description of the exception as a QString QString qwhat() const noexcept; ///< Return the description of the exception as a QString
const char *what() const noexcept override; ///< Return the description of the exception as C style string const char *what() const noexcept override; ///< Return the description of the exception as C style string
QString details() const noexcept; ///< Return the details for the exception
private: // data members private: // data members
QString const what_; ///< The description of the exception QString const what_; ///< The description of the exception.
QString const details_; ///< The optional details for the exception.
}; };

View File

@ -88,8 +88,9 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMon
} }
GRPCConfig sc; GRPCConfig sc;
if (!sc.load(path)) { QString err;
throw Exception("The gRPC service configuration file is invalid."); if (!sc.load(path, &err)) {
throw Exception("The gRPC service configuration file is invalid.", err);
} }
return sc; return sc;
@ -105,11 +106,10 @@ void GRPCClient::setLog(Log *log) {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[out] outError If the function returns false, this variable contains a description of the error.
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null. /// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
/// \return true iff the connection was successful. /// \return true iff the connection was successful.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess, QString &outError) { void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess) {
try { try {
serverToken_ = config.token.toStdString(); serverToken_ = config.token.toStdString();
QString address; QString address;
@ -158,9 +158,10 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
this->logInfo("Successfully connected to gRPC server."); this->logInfo("Successfully connected to gRPC server.");
QString const clientToken = QUuid::createUuid().toString(); QString const clientToken = QUuid::createUuid().toString();
QString clientConfigPath = createClientConfigFile(clientToken); QString error;
QString clientConfigPath = createClientConfigFile(clientToken, &error);
if (clientConfigPath.isEmpty()) { if (clientConfigPath.isEmpty()) {
throw Exception("gRPC client config could not be saved."); throw Exception("gRPC client config could not be saved.", error);
} }
this->logInfo(QString("Client config file was saved to '%1'").arg(QDir::toNativeSeparators(clientConfigPath))); this->logInfo(QString("Client config file was saved to '%1'").arg(QDir::toNativeSeparators(clientConfigPath)));
@ -176,12 +177,9 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
} }
log_->info("gRPC token was validated"); log_->info("gRPC token was validated");
return true;
} }
catch (Exception const &e) { catch (Exception const &e) {
outError = e.qwhat(); throw Exception("Cannot connect to Go backend via gRPC: " + e.qwhat(), e.details());
return false;
} }
} }

View File

@ -59,7 +59,7 @@ public: // member functions.
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator. GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator. GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
void setLog(Log *log); ///< Set the log for the client. void setLog(Log *log); ///< Set the log for the client.
bool connectToServer(GRPCConfig const &config, class ProcessMonitor *serverProcess, QString &outError); ///< Establish connection to the gRPC server. void connectToServer(GRPCConfig const &config, class ProcessMonitor *serverProcess); ///< Establish connection to the gRPC server.
grpc::Status checkTokens(QString const &clientConfigPath, QString &outReturnedClientToken); ///< Performs a token check. grpc::Status checkTokens(QString const &clientConfigPath, QString &outReturnedClientToken); ///< Performs a token check.
grpc::Status addLogEntry(Log::Level level, QString const &package, QString const &message); ///< Performs the "AddLogEntry" gRPC call. grpc::Status addLogEntry(Log::Level level, QString const &package, QString const &message); ///< Performs the "AddLogEntry" gRPC call.

View File

@ -25,8 +25,7 @@ using namespace bridgepp;
namespace { namespace {
Exception const invalidFileException("The service configuration file is invalid"); // Exception for invalid config. Exception const invalidFileException("The content of the service configuration file is invalid"); // Exception for invalid config.
Exception const couldNotSaveException("The service configuration file could not be saved"); ///< Exception for write errors.
QString const keyPort = "port"; ///< The JSON key for the port. QString const keyPort = "port"; ///< The JSON key for the port.
QString const keyCert = "cert"; ///< The JSON key for the TLS certificate. QString const keyCert = "cert"; ///< The JSON key for the TLS certificate.
QString const keyToken = "token"; ///< The JSON key for the identification token. QString const keyToken = "token"; ///< The JSON key for the identification token.
@ -78,8 +77,11 @@ qint32 jsonIntValue(QJsonObject const &object, QString const &key) {
bool GRPCConfig::load(QString const &path, QString *outError) { bool GRPCConfig::load(QString const &path, QString *outError) {
try { try {
QFile file(path); QFile file(path);
if (!file.exists())
throw Exception("The file service configuration file does not exist.");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
throw Exception("Could not open gRPC service config file."); throw Exception("The file exists but cannot be opened.");
} }
QJsonDocument const doc = QJsonDocument::fromJson(file.readAll()); QJsonDocument const doc = QJsonDocument::fromJson(file.readAll());
@ -93,7 +95,7 @@ bool GRPCConfig::load(QString const &path, QString *outError) {
} }
catch (Exception const &e) { catch (Exception const &e) {
if (outError) { if (outError) {
*outError = e.qwhat(); *outError = QString("Error loading gRPC service configuration file '%1'.\n%2").arg(QFileInfo(path).absoluteFilePath(), e.qwhat());
} }
return false; return false;
} }
@ -115,19 +117,19 @@ bool GRPCConfig::save(QString const &path, QString *outError) {
QFile file(path); QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
throw couldNotSaveException; throw Exception("The file could not be opened for writing.");
} }
QByteArray const array = QJsonDocument(object).toJson(); QByteArray const array = QJsonDocument(object).toJson();
if (array.size() != file.write(array)) { if (array.size() != file.write(array)) {
throw couldNotSaveException; throw Exception("An error occurred while writing to the file.");
} }
return true; return true;
} }
catch (Exception const &e) { catch (Exception const &e) {
if (outError) { if (outError) {
*outError = e.qwhat(); *outError = QString("Error saving gRPC service configuration file '%1'.\n%2").arg(QFileInfo(path).absoluteFilePath(), e.qwhat());
} }
return false; return false;
} }

View File

@ -76,10 +76,12 @@ QString grpcClientConfigBasePath() {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] token The token to put in the file. /// \param[in] token The token to put in the file.
/// \param[out] outError if the function returns an empty string and this pointer is not null, the pointer variable holds a description of the error
/// on exit.
/// \return The path of the created file. /// \return The path of the created file.
/// \return A null string if the file could not be saved.. /// \return A null string if the file could not be saved.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
QString createClientConfigFile(QString const &token) { QString createClientConfigFile(QString const &token, QString *outError) {
QString const basePath = grpcClientConfigBasePath(); QString const basePath = grpcClientConfigBasePath();
QString path, error; QString path, error;
for (qint32 i = 0; i < 1000; ++i) // we try a decent amount of times for (qint32 i = 0; i < 1000; ++i) // we try a decent amount of times
@ -88,13 +90,16 @@ QString createClientConfigFile(QString const &token) {
if (!QFileInfo(path).exists()) { if (!QFileInfo(path).exists()) {
GRPCConfig config; GRPCConfig config;
config.token = token; config.token = token;
if (!config.save(path)) {
if (!config.save(path, outError)) {
return QString(); return QString();
} }
return path; return path;
} }
} }
if (outError)
*outError = "no usable client configuration file name could be found.";
return QString(); return QString();
} }

View File

@ -36,7 +36,7 @@ typedef std::shared_ptr<grpc::StreamEvent> SPStreamEvent; ///< Type definition f
QString grpcServerConfigPath(); ///< Return the path of the gRPC server config file. QString grpcServerConfigPath(); ///< Return the path of the gRPC server config file.
QString grpcClientConfigBasePath(); ///< Return the path of the gRPC client config file. QString grpcClientConfigBasePath(); ///< Return the path of the gRPC client config file.
QString createClientConfigFile(QString const &token); ///< Create the client config file the server will retrieve and return its path. QString createClientConfigFile(QString const &token, QString *outError); ///< Create the client config file the server will retrieve and return its path.
grpc::LogLevel logLevelToGRPC(Log::Level level); ///< Convert a Log::Level to gRPC enum value. grpc::LogLevel logLevelToGRPC(Log::Level level); ///< Convert a Log::Level to gRPC enum value.
Log::Level logLevelFromGRPC(grpc::LogLevel level); ///< Convert a grpc::LogLevel to a Log::Level. Log::Level logLevelFromGRPC(grpc::LogLevel level); ///< Convert a grpc::LogLevel to a Log::Level.
grpc::UserState userStateToGRPC(UserState state); ///< Convert a bridgepp::UserState to a grpc::UserState. grpc::UserState userStateToGRPC(UserState state); ///< Convert a bridgepp::UserState to a grpc::UserState.