Compare commits

...

9 Commits

Author SHA1 Message Date
c6f1f159f3 chore: Bridge Perth Narrows 3.0.18 2023-02-28 06:53:16 +01:00
82af4e01bc feat(GODT-2364): wait and retry once if the gRPC service config file exists but cannot be opened. 2023-02-28 06:21:36 +01:00
9ad5f74409 feat(GODT-2364): added optional details to C++ exceptions. 2023-02-28 06:21:25 +01:00
10cf153678 fix(GODT-2413): use qEnvironmentVariable() instead of qgetenv(). 2023-02-27 15:41:26 +01:00
5ba07db7e3 chore: Bump Gluon for GODT-2399, GODT-2400 and GODT-2414
fix(GODT-2399): Defer updated message deletion
fix(GODT-2400): Allow state updates to be applied if command fails
fix(GODT-2414): Multiple deletion bug in WriteControlledStore
2023-02-27 14:53:37 +01:00
ad0d4ebd36 fix(GODT-2412): Don't treat context cancellation as BadEvent 2023-02-27 14:34:35 +01:00
9f3c14ab1e fix(GODT-2404): Handle unexpected EOF
When fetching too many attachment bodies at once, the read can fail with
io.ErrUnexpectedEOF. In that case, we returun an error so the fetch is retried.
2023-02-27 14:33:44 +01:00
74cf5d422b fix(GODT-2390): Missing changes from pervious commit
Always reports error type to sentry.

Add error checks for get event as well.
2023-02-27 14:33:38 +01:00
dcf694588c fix(GODT-2390): Add reports for uncaught json and net.opErr
Report to sentry if we see some uncaught network err, but don't force
the user logout.

If we catch an uncaught json parser error we report the error to sentry
and let the user be logged out later.

Finally this patch also prints the error type in UserBadEvent sentry
report to further help diagnose issues.
2023-02-27 14:33:21 +01:00
21 changed files with 201 additions and 68 deletions

View File

@ -2,6 +2,21 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 3.0.18] Perth Narrows
### Fixed
* GODT-2364: Wait and retry once if the gRPC service config file exists but cannot be opened.
* GODT-2364: Added optional details to C++ exceptions.
* GODT-2413: Use qEnvironmentVariable() instead of qgetenv().
* GODT-2412: Don't treat context cancellation as BadEvent.
* GODT-2404: Handle unexpected EOF.
* GODT-2400: Allow state updates to be applied if command fails.
* GODT-2399: Fix immediate message deletion during updates.
* GODT-2390: Missing changes from pervious commit.
* GODT-2390: Add reports for uncaught json and net.opErr.
* GODT-2414: Multiple deletion bug in WriteControlledStore.
## [Bridge 3.0.18] Perth Narrows ## [Bridge 3.0.18] Perth Narrows
### Fixed ### Fixed

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.PHONY: build build-gui build-nogui build-launcher versioner hasher .PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository. # Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.0.18+git BRIDGE_APP_VERSION?=3.0.19+git
APP_VERSION:=${BRIDGE_APP_VERSION} APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG APP_VENDOR:=Proton AG

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.18
require ( require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.1.1 github.com/Masterminds/semver/v3 v3.1.1
github.com/ProtonMail/gluon v0.14.2-0.20230221144759-b277a90ca303 github.com/ProtonMail/gluon v0.14.2-0.20230227135029-cef8f5824680
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.3.1-0.20230209110241-fe7894c4931a github.com/ProtonMail/go-proton-api v0.3.1-0.20230209110241-fe7894c4931a
github.com/ProtonMail/go-rfc5322 v0.11.0 github.com/ProtonMail/go-rfc5322 v0.11.0

4
go.sum
View File

@ -28,8 +28,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.14.2-0.20230221144759-b277a90ca303 h1:OTzqa4dcVuo4O4Kkng8rcoKJiKEY9HalQb8Y3OJWNFA= github.com/ProtonMail/gluon v0.14.2-0.20230227135029-cef8f5824680 h1:NGp7LfbsKePRHBgMcgquycHx3CSuS7255i0wanAiCuY=
github.com/ProtonMail/gluon v0.14.2-0.20230221144759-b277a90ca303/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q= github.com/ProtonMail/gluon v0.14.2-0.20230227135029-cef8f5824680/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=

View File

@ -22,6 +22,7 @@ import (
"fmt" "fmt"
"github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/user"
@ -56,6 +57,9 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
case events.UserBadEvent: case events.UserBadEvent:
bridge.handleUserBadEvent(ctx, user, event.Error) bridge.handleUserBadEvent(ctx, user, event.Error)
case events.UncategorizedEventError:
bridge.handleUncategorizedErrorEvent(event)
} }
return nil return nil
@ -139,7 +143,8 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, err error) { func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, err error) {
safe.Lock(func() { safe.Lock(func() {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{ if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
"error": err, "error_type": fmt.Sprintf("%T", internal.ErrCause(err)),
"error": err,
}); rerr != nil { }); rerr != nil {
logrus.WithError(rerr).Error("Failed to report failed event handling") logrus.WithError(rerr).Error("Failed to report failed event handling")
} }
@ -147,3 +152,12 @@ func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, e
bridge.logoutUser(ctx, user, true, false) bridge.logoutUser(ctx, user, true, false)
}, bridge.usersLock) }, bridge.usersLock)
} }
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
"error_type": fmt.Sprintf("%T", internal.ErrCause(event.Error)),
"error": event.Error,
}); rerr != nil {
logrus.WithError(rerr).Error("Failed to report failed event handling")
}
}

31
internal/errors.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package internal
import "errors"
// ErrCause returns the cause of the error, the inner-most error in the wrapped chain.
func ErrCause(err error) error {
cause := err
for errors.Unwrap(cause) != nil {
cause = errors.Unwrap(cause)
}
return cause
}

View File

@ -156,3 +156,14 @@ type AddressModeChanged struct {
func (event AddressModeChanged) String() string { func (event AddressModeChanged) String() string {
return fmt.Sprintf("AddressModeChanged: UserID: %s, AddressMode: %s", event.UserID, event.AddressMode) return fmt.Sprintf("AddressModeChanged: UserID: %s, AddressMode: %s", event.UserID, event.AddressMode)
} }
type UncategorizedEventError struct {
eventBase
UserID string
Error error
}
func (event UncategorizedEventError) String() string {
return fmt.Sprintf("UncategorizedEventError: UserID: %s, Source:%T, Error: %s", event.UserID, event.Error, event.Error)
}

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

@ -71,20 +71,20 @@ std::mt19937_64 &rng() {
QString userConfigDir() { QString userConfigDir() {
QString dir; QString dir;
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
dir = qgetenv ("AppData"); dir = qEnvironmentVariable("AppData");
if (dir.isEmpty()) if (dir.isEmpty())
throw Exception("%AppData% is not defined."); throw Exception("%AppData% is not defined.");
#elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN) #elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN)
dir = qgetenv("HOME"); dir = qEnvironmentVariable("HOME");
if (dir.isEmpty()) { if (dir.isEmpty()) {
throw Exception("$HOME is not defined."); throw Exception("$HOME is not defined.");
} }
dir += "/Library/Application Support"; dir += "/Library/Application Support";
#else #else
dir = qgetenv ("XDG_CONFIG_HOME"); dir = qEnvironmentVariable("XDG_CONFIG_HOME");
if (dir.isEmpty()) if (dir.isEmpty())
{ {
dir = qgetenv ("HOME"); dir = qEnvironmentVariable("HOME");
if (dir.isEmpty()) if (dir.isEmpty())
throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined"); throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined");
dir += "/.config"; dir += "/.config";
@ -104,20 +104,20 @@ QString userCacheDir() {
QString dir; QString dir;
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
dir = qgetenv ("LocalAppData"); dir = qEnvironmentVariable("LocalAppData");
if (dir.isEmpty()) if (dir.isEmpty())
throw Exception("%LocalAppData% is not defined."); throw Exception("%LocalAppData% is not defined.");
#elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN) #elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN)
dir = qgetenv("HOME"); dir = qEnvironmentVariable("HOME");
if (dir.isEmpty()) { if (dir.isEmpty()) {
throw Exception("$HOME is not defined."); throw Exception("$HOME is not defined.");
} }
dir += "/Library/Caches"; dir += "/Library/Caches";
#else #else
dir = qgetenv ("XDG_CACHE_HOME"); dir = qEnvironmentVariable("XDG_CACHE_HOME");
if (dir.isEmpty()) if (dir.isEmpty())
{ {
dir = qgetenv ("HOME"); dir = qEnvironmentVariable("HOME");
if (dir.isEmpty()) if (dir.isEmpty())
throw Exception("neither $XDG_CACHE_HOME nor $HOME are defined"); throw Exception("neither $XDG_CACHE_HOME nor $HOME are defined");
dir += "/.cache"; dir += "/.cache";
@ -138,10 +138,10 @@ QString userDataDir() {
QString folder; QString folder;
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
QString dir = qgetenv ("XDG_DATA_HOME"); QString dir = qEnvironmentVariable("XDG_DATA_HOME");
if (dir.isEmpty()) if (dir.isEmpty())
{ {
dir = qgetenv ("HOME"); dir = qEnvironmentVariable("HOME");
if (dir.isEmpty()) if (dir.isEmpty())
throw Exception("neither $XDG_DATA_HOME nor $HOME are defined"); throw Exception("neither $XDG_DATA_HOME nor $HOME are defined");
dir += "/.local/share"; dir += "/.local/share";

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,14 @@ 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 gRPC 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."); QThread::msleep(500); // we wait a bit and retry once, just in case server is not done writing/moving the config file.
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
throw Exception("The gRPC service configuration file exists but cannot be opened.");
}
} }
QJsonDocument const doc = QJsonDocument::fromJson(file.readAll()); QJsonDocument const doc = QJsonDocument::fromJson(file.readAll());
@ -93,7 +98,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 +120,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.

View File

@ -20,10 +20,11 @@ package user
import ( import (
"context" "context"
"crypto/subtle" "crypto/subtle"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net/url" "net"
"strings" "strings"
"sync/atomic" "sync/atomic"
"time" "time"
@ -33,6 +34,7 @@ import (
"github.com/ProtonMail/gluon/queue" "github.com/ProtonMail/gluon/queue"
"github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal"
"github.com/ProtonMail/proton-bridge/v3/internal/async" "github.com/ProtonMail/proton-bridge/v3/internal/async"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/logging"
@ -626,7 +628,7 @@ func (user *User) doEventPoll(ctx context.Context) error {
event, err := user.client.GetEvent(ctx, user.vault.EventID()) event, err := user.client.GetEvent(ctx, user.vault.EventID())
if err != nil { if err != nil {
return fmt.Errorf("failed to get event: %w", err) return fmt.Errorf("failed to get event (caused by %T): %w", internal.ErrCause(err), err)
} }
// If the event ID hasn't changed, there are no new events. // If the event ID hasn't changed, there are no new events.
@ -642,14 +644,44 @@ func (user *User) doEventPoll(ctx context.Context) error {
// Handle the event. // Handle the event.
if err := user.handleAPIEvent(ctx, event); err != nil { if err := user.handleAPIEvent(ctx, event); err != nil {
// If the error is a context cancellation, return error to retry later.
if errors.Is(err, context.Canceled) {
return fmt.Errorf("failed to handle event due to context cancellation: %w", err)
}
// If the error is a network error, return error to retry later. // If the error is a network error, return error to retry later.
if netErr := new(proton.NetError); errors.As(err, &netErr) { if netErr := new(proton.NetError); errors.As(err, &netErr) {
return fmt.Errorf("failed to handle event due to network issue: %w", err) return fmt.Errorf("failed to handle event due to network issue: %w", err)
} }
// If the error is a url.Error, return error to retry later. // Catch all for uncategorized net errors that may slip through.
if urlErr := new(url.Error); errors.As(err, &urlErr) { if netErr := new(net.OpError); errors.As(err, &netErr) {
return fmt.Errorf("failed to handle event due to URL issue: %w", err) user.eventCh.Enqueue(events.UncategorizedEventError{
UserID: user.ID(),
Error: err,
})
return fmt.Errorf("failed to handle event due to network issues (uncategorized): %w", err)
}
// In case a json decode error slips through.
if jsonErr := new(json.UnmarshalTypeError); errors.As(err, &jsonErr) {
user.eventCh.Enqueue(events.UncategorizedEventError{
UserID: user.ID(),
Error: err,
})
return fmt.Errorf("failed to handle event due to JSON issue: %w", err)
}
// If the error is an unexpected EOF, return error to retry later.
if errors.Is(err, io.ErrUnexpectedEOF) {
user.eventCh.Enqueue(events.UncategorizedEventError{
UserID: user.ID(),
Error: err,
})
return fmt.Errorf("failed to handle event due to EOF: %w", err)
} }
// If the error is a server-side issue, return error to retry later. // If the error is a server-side issue, return error to retry later.