chore: merge release/perth_narrows into devel

This commit is contained in:
Jakub
2023-03-13 11:39:04 +01:00
159 changed files with 8308 additions and 1899 deletions

View File

@ -1,8 +1,8 @@
find_program(QMAKE_EXE "qmake")
find_program(QMAKE_EXE NAMES "qmake" "qmake6")
if (NOT QMAKE_EXE)
message(FATAL_ERROR "Could not locate qmake executable, make sur you have Qt 6 installed in that qmake is in your PATH environment variable.")
message(FATAL_ERROR "Could not locate qmake executable, make sure you have Qt 6 installed in that qmake is in your PATH environment variable.")
endif()
message(STATUS "Found qmake at ${QMAKE_EXE}")
execute_process(COMMAND "${QMAKE_EXE}" -query QT_INSTALL_PREFIX OUTPUT_VARIABLE QT_DIR OUTPUT_STRIP_TRAILING_WHITESPACE)
set(CMAKE_PREFIX_PATH ${QT_DIR} ${CMAKE_PREFIX_PATH})
set(CMAKE_PREFIX_PATH ${QT_DIR} ${CMAKE_PREFIX_PATH})

View File

@ -20,6 +20,7 @@
#include "Cert.h"
#include "GRPCService.h"
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/BridgeUtils.h>
#include <bridgepp/GRPC/GRPCUtils.h>
#include <bridgepp/GRPC/GRPCConfig.h>
@ -33,7 +34,6 @@ using namespace grpc;
//****************************************************************************************************************************************************
GRPCServerWorker::GRPCServerWorker(QObject *parent)
: Worker(parent) {
}
@ -81,7 +81,7 @@ void GRPCServerWorker::run() {
config.port = port;
QString err;
if (!config.save(grpcServerConfigPath(), &err)) {
if (!config.save(grpcServerConfigPath(bridgepp::userConfigDir()), &err)) {
throw Exception(QString("Could not save gRPC server config. %1").arg(err));
}

View File

@ -192,17 +192,6 @@ Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *request, Bool
}
//****************************************************************************************************************************************************
/// \param[out] response The response.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::GoOs(ServerContext *, Empty const *, StringValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().settingsTab().os().toStdString());
return Status::OK;
}
//****************************************************************************************************************************************************
/// \return The status for the call.
//****************************************************************************************************************************************************

View File

@ -51,7 +51,6 @@ public: // member functions.
grpc::Status IsBetaEnabled(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
grpc::Status SetIsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
grpc::Status GoOs(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;

View File

@ -52,7 +52,11 @@ UsersTab::UsersTab(QWidget *parent)
connect(ui_.tableUserList, &QTableView::doubleClicked, this, &UsersTab::onEditUserButton);
connect(ui_.buttonRemoveUser, &QPushButton::clicked, this, &UsersTab::onRemoveUserButton);
connect(ui_.buttonUserBadEvent, &QPushButton::clicked, this, &UsersTab::onSendUserBadEvent);
connect(ui_.buttonImapLoginFailed, &QPushButton::clicked, this, &UsersTab::onSendIMAPLoginFailedEvent);
connect(ui_.buttonUsedBytesChanged, &QPushButton::clicked, this, &UsersTab::onSendUsedBytesChangedEvent);
connect(ui_.checkUsernamePasswordError, &QCheckBox::toggled, this, &UsersTab::updateGUIState);
connect(ui_.checkSync, &QCheckBox::toggled, this, &UsersTab::onCheckSyncToggled);
connect(ui_.sliderSync, &QSlider::valueChanged, this, &UsersTab::onSliderSyncValueChanged);
users_.append(randomUser());
@ -155,16 +159,76 @@ void UsersTab::onSendUserBadEvent() {
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UsersTab::onSendUsedBytesChangedEvent() {
SPUser const user = selectedUser();
int const index = this->selectedIndex();
if (!user) {
app().log().error(QString("%1 failed. Unkown user.").arg(__FUNCTION__));
return;
}
if (UserState::Connected != user->state()) {
app().log().error(QString("%1 failed. User is not connected").arg(__FUNCTION__));
}
qint64 const usedBytes = qint64(ui_.spinUsedBytes->value());
user->setUsedBytes(usedBytes);
users_.touch(index);
GRPCService &grpc = app().grpc();
if (grpc.isStreaming()) {
QString const userID = user->id();
grpc.sendEvent(newUsedBytesChangedEvent(userID, usedBytes));
}
this->updateGUIState();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UsersTab::onSendIMAPLoginFailedEvent() {
GRPCService &grpc = app().grpc();
if (grpc.isStreaming()) {
grpc.sendEvent(newIMAPLoginFailedEvent(ui_.editIMAPLoginFailedUsername->text()));
}
this->updateGUIState();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UsersTab::updateGUIState() {
SPUser const user = selectedUser();
bool const hasSelectedUser = user.get();
UserState const state = user ? user->state() : UserState::SignedOut;
ui_.buttonEditUser->setEnabled(hasSelectedUser);
ui_.buttonRemoveUser->setEnabled(hasSelectedUser);
ui_.groupBoxBadEvent->setEnabled(hasSelectedUser && (UserState::SignedOut != user->state()));
ui_.groupBoxBadEvent->setEnabled(hasSelectedUser && (UserState::SignedOut != state));
ui_.groupBoxUsedSpace->setEnabled(hasSelectedUser && (UserState::Connected == state));
ui_.editUsernamePasswordError->setEnabled(ui_.checkUsernamePasswordError->isChecked());
ui_.spinUsedBytes->setValue(user ? user->usedBytes() : 0.0);
ui_.groupboxSync->setEnabled(user.get());
if (user)
ui_.editIMAPLoginFailedUsername->setText(user->primaryEmailOrUsername());
QSignalBlocker b(ui_.checkSync);
bool const syncing = user && user->isSyncing();
ui_.checkSync->setChecked(syncing);
b = QSignalBlocker(ui_.sliderSync);
ui_.sliderSync->setEnabled(syncing);
qint32 const progressPercent = syncing ? qint32(user->syncProgress() * 100.0f) : 0;
ui_.sliderSync->setValue(progressPercent);
ui_.labelSync->setText(syncing ? QString("%1%").arg(progressPercent) : "" );
}
@ -368,3 +432,44 @@ void UsersTab::processBadEventUserFeedback(QString const &userID, bool doResync)
this->updateGUIState();
}
//****************************************************************************************************************************************************
/// \param[in] checked Is the sync checkbox checked?
//****************************************************************************************************************************************************
void UsersTab::onCheckSyncToggled(bool checked) {
SPUser const user = this->selectedUser();
if ((!user) || (user->isSyncing() == checked)) {
return;
}
user->setIsSyncing(checked);
user->setSyncProgress(0.0);
GRPCService &grpc = app().grpc();
// we do not apply delay for these event.
if (checked) {
grpc.sendEvent(newSyncStartedEvent(user->id()));
grpc.sendEvent(newSyncProgressEvent(user->id(), 0.0, 1, 1));
} else {
grpc.sendEvent(newSyncFinishedEvent(user->id()));
}
this->updateGUIState();
}
//****************************************************************************************************************************************************
/// \param[in] value The value for the slider.
//****************************************************************************************************************************************************
void UsersTab::onSliderSyncValueChanged(int value) {
SPUser const user = this->selectedUser();
if ((!user) || (!user->isSyncing()) || user->syncProgress() == value) {
return;
}
double const progress = value / 100.0;
user->setSyncProgress(progress);
app().grpc().sendEvent(newSyncProgressEvent(user->id(), progress, 1, 1)); // we do not simulate elapsed & remaining.
this->updateGUIState();
}

View File

@ -62,6 +62,10 @@ private slots:
void onRemoveUserButton(); ///< Remove the currently selected user.
void onSelectionChanged(QItemSelection, QItemSelection); ///< Slot for the change of the selection.
void onSendUserBadEvent(); ///< Slot for the 'Send Bad Event Error' button.
void onSendUsedBytesChangedEvent(); ///< Slot for the 'Send Used Bytes Changed Event' button.
void onSendIMAPLoginFailedEvent(); ///< Slot for the 'Send IMAP Login failure Event' button.
void onCheckSyncToggled(bool checked); ///< Slot for the 'Synchronizing' check box.
void onSliderSyncValueChanged(int value); ///< Slot for the sync 'Progress' slider.
void updateGUIState(); ///< Update the GUI state.
private: // member functions.

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1225</width>
<height>717</height>
<width>1221</width>
<height>894</height>
</rect>
</property>
<property name="windowTitle">
@ -66,6 +66,52 @@
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="groupboxSync">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Sync</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,0">
<item>
<widget class="QCheckBox" name="checkSync">
<property name="text">
<string>Synchronizing</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelSync">
<property name="text">
<string>0%</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSlider" name="sliderSync">
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxBadEvent">
<property name="minimumSize">
@ -80,13 +126,6 @@
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="labelUserBadEvent">
<property name="text">
<string>Message: </string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="editUserBadEvent">
<property name="minimumSize">
@ -96,18 +135,102 @@
</size>
</property>
<property name="text">
<string>Bad event error.</string>
<string/>
</property>
<property name="placeholderText">
<string>error message</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonUserBadEvent">
<property name="text">
<string>Send</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxUsedSpace">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Used Bytes Changed</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QPushButton" name="buttonUserBadEvent">
<property name="text">
<string>Send Bad Event Error</string>
</property>
</widget>
<layout class="QHBoxLayout" name="hBoxUsedBytes" stretch="1,0">
<item>
<widget class="QDoubleSpinBox" name="spinUsedBytes">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
</property>
<property name="decimals">
<number>0</number>
</property>
<property name="maximum">
<double>1000000000000000.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonUsedBytesChanged">
<property name="text">
<string>Send</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxIMAPLoginFailed">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>IMAP Login Failure</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLineEdit" name="editIMAPLoginFailedUsername">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>username or primary email</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonImapLoginFailed">
<property name="text">
<string>Send</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>

View File

@ -85,7 +85,10 @@ int main(int argc, char **argv) {
return exitCode;
}
catch (Exception const &e) {
QTextStream(stderr) << QString("A fatal error occurred: %1\n").arg(e.qwhat());
QString message = e.qwhat();
if (!e.details().isEmpty())
message += "\n\nDetails:\n" + e.details();
QTextStream(stderr) << QString("A fatal error occurred: %1\n").arg(message);
return EXIT_FAILURE;
}
}

View File

@ -19,6 +19,7 @@
#include "AppController.h"
#include "QMLBackend.h"
#include "SentryUtils.h"
#include "Settings.h"
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/ProcessMonitor.h>
@ -48,10 +49,19 @@ AppController &app() {
AppController::AppController()
: backend_(std::make_unique<QMLBackend>())
, grpc_(std::make_unique<GRPCClient>())
, log_(std::make_unique<Log>()) {
, log_(std::make_unique<Log>())
, settings_(new Settings) {
}
//****************************************************************************************************************************************************
// The following is in the implementation file because of unique pointers with incomplete types in headers.
// See https://stackoverflow.com/questions/6012157/is-stdunique-ptrt-required-to-know-the-full-definition-of-t
//****************************************************************************************************************************************************
AppController::~AppController() = default;
//****************************************************************************************************************************************************
/// \return The bridge worker, which can be null if the application was run in 'attach' mode (-a command-line switch).
//****************************************************************************************************************************************************
@ -71,6 +81,14 @@ ProcessMonitor *AppController::bridgeMonitor() const {
}
//****************************************************************************************************************************************************
/// \return A reference to the application settings.
//****************************************************************************************************************************************************
Settings &AppController::settings() {
return *settings_;
}
//****************************************************************************************************************************************************
/// \param[in] exception The exception that triggered the fatal error.
//****************************************************************************************************************************************************
@ -102,4 +120,4 @@ void AppController::restart(bool isCrashing) {
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
launcher_ = launcher;
launcherArgs_ = args;
}
}

View File

@ -20,8 +20,9 @@
#define BRIDGE_GUI_APP_CONTROLLER_H
// @formatter:off
//@formatter:off
class QMLBackend;
class Settings;
namespace bridgepp {
class Log;
class Overseer;
@ -29,7 +30,7 @@ class GRPCClient;
class ProcessMonitor;
class Exception;
}
// @formatter:off
//@formatter:on
//****************************************************************************************************************************************************
@ -42,7 +43,7 @@ Q_OBJECT
public: // member functions.
AppController(AppController const &) = delete; ///< Disabled copy-constructor.
AppController(AppController &&) = delete; ///< Disabled assignment copy-constructor.
~AppController() override = default; ///< Destructor.
~AppController() override; ///< Destructor.
AppController &operator=(AppController const &) = delete; ///< Disabled assignment operator.
AppController &operator=(AppController &&) = delete; ///< Disabled move assignment operator.
QMLBackend &backend() { return *backend_; } ///< Return a reference to the backend.
@ -50,7 +51,8 @@ public: // member functions.
bridgepp::Log &log() { return *log_; } ///< Return a reference to the log.
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
void setLauncherArgs(const QString& launcher, const QStringList& args);
Settings &settings();; ///< Return the application settings.
void setLauncherArgs(const QString &launcher, const QStringList &args);
public slots:
void onFatalError(bridgepp::Exception const& e); ///< Handle fatal errors.
@ -64,6 +66,7 @@ private: // data members
std::unique_ptr<bridgepp::GRPCClient> grpc_; ///< The RPC client.
std::unique_ptr<bridgepp::Log> log_; ///< The log.
std::unique_ptr<bridgepp::Overseer> bridgeOverseer_; ///< The overseer for the bridge monitor worker.
std::unique_ptr<Settings> settings_; ///< The application settings.
QString launcher_;
QStringList launcherArgs_;
};

View File

@ -19,12 +19,13 @@
#ifndef BRIDGE_GUI_VERSION_H
#define BRIDGE_GUI_VERSION_H
#define PROJECT_FULL_NAME "@BRIDGE_APP_FULL_NAME@"
#define PROJECT_VENDOR "@BRIDGE_VENDOR@"
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
#define PROJECT_REVISION "@BRIDGE_REVISION@"
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
#define PROJECT_DSN_SENTRY "@BRIDGE_DSN_SENTRY@"
#define PROJECT_BUILD_ENV "@BRIDGE_BUILD_ENV@"
#define PROJECT_FULL_NAME "@BRIDGE_APP_FULL_NAME@"
#define PROJECT_VENDOR "@BRIDGE_VENDOR@"
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
#define PROJECT_REVISION "@BRIDGE_REVISION@"
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
#define PROJECT_DSN_SENTRY "@BRIDGE_DSN_SENTRY@"
#define PROJECT_BUILD_ENV "@BRIDGE_BUILD_ENV@"
#define PROJECT_CRASHPAD_HANDLER_PATH "@BRIDGE_CRASHPAD_HANDLER_PATH@"
#endif // BRIDGE_GUI_VERSION_H

View File

@ -85,7 +85,6 @@ message(STATUS "Using Qt ${Qt6_VERSION}")
#*****************************************************************************************************************************************************
find_package(sentry CONFIG REQUIRED)
#*****************************************************************************************************************************************************
# Source files and output
#*****************************************************************************************************************************************************
@ -110,24 +109,23 @@ add_executable(bridge-gui
Resources.qrc
AppController.cpp AppController.h
BridgeApp.cpp BridgeApp.h
BuildConfig.h
CommandLine.cpp CommandLine.h
EventStreamWorker.cpp EventStreamWorker.h
LogUtils.cpp LogUtils.h
main.cpp
Pch.h
BuildConfig.h
QMLBackend.cpp QMLBackend.h
UserList.cpp UserList.h
SentryUtils.cpp SentryUtils.h
Settings.cpp Settings.h
${DOCK_ICON_SRC_FILE} MacOS/DockIcon.h
)
if (APPLE)
target_sources(bridge-gui PRIVATE MacOS/SecondInstance.mm MacOS/SecondInstance.h)
endif(APPLE)
if (WIN32) # on Windows, we add a (non-Qt) resource file that contains the application icon and version information.
string(TIMESTAMP BRIDGE_BUILD_YEAR "%Y")
set(REGEX_NUMBER "[0123456789]") # CMake matches does not support \d.

View File

@ -17,6 +17,7 @@
#include "LogUtils.h"
#include "BuildConfig.h"
#include <bridgepp/BridgeUtils.h>
@ -24,9 +25,52 @@ using namespace bridgepp;
namespace {
qsizetype const logFileTailMaxLength = 25 * 1024; ///< The maximum length of the portion of log returned by tailOfLatestBridgeLog()
qsizetype const logFileTailMaxLength = 25 * 1024; ///< The maximum length of the portion of log returned by tailOfLatestBridgeLog()
}
//****************************************************************************************************************************************************
/// \return user logs directory used by bridge.
//****************************************************************************************************************************************************
QString userLogsDir() {
QString const path = QDir(bridgepp::userDataDir()).absoluteFilePath("logs");
QDir().mkpath(path);
return path;
}
//****************************************************************************************************************************************************
/// \return A reference to the log.
//****************************************************************************************************************************************************
Log &initLog() {
Log &log = app().log();
log.registerAsQtMessageHandler();
log.setEchoInConsole(true);
// remove old gui log files
QDir const logsDir(userLogsDir());
for (QFileInfo const fileInfo: logsDir.entryInfoList({ "gui_v*.log" }, QDir::Filter::Files)) { // entryInfolist apparently only support wildcards, not regex.
QFile(fileInfo.absoluteFilePath()).remove();
}
// create new GUI log file
QString error;
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error)) {
log.error(error);
}
log.info("bridge-gui starting");
QString const qtCompileTimeVersion = QT_VERSION_STR;
QString const qtRuntimeVersion = qVersion();
QString msg = QString("Using Qt %1").arg(qtRuntimeVersion);
if (qtRuntimeVersion != qtCompileTimeVersion) {
msg += QString(" (compiled against %1)").arg(qtCompileTimeVersion);
}
log.info(msg);
return log;
}
//****************************************************************************************************************************************************
/// \brief Return the path of the latest bridge log.
/// \return The path of the latest bridge log file.
@ -58,5 +102,3 @@ QByteArray tailOfLatestBridgeLog() {
return file.open(QIODevice::Text | QIODevice::ReadOnly) ? file.readAll().right(logFileTailMaxLength) : QByteArray();
}

View File

@ -20,6 +20,10 @@
#define BRIDGE_GUI_LOG_UTILS_H
#include <bridgepp/Log/Log.h>
bridgepp::Log &initLog(); ///< Initialize the application log.
QByteArray tailOfLatestBridgeLog(); ///< Return the last bytes of the last bridge log.

View File

@ -17,11 +17,12 @@
#include "QMLBackend.h"
#include "EventStreamWorker.h"
#include "BuildConfig.h"
#include "EventStreamWorker.h"
#include "LogUtils.h"
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/BridgeUtils.h>
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Worker/Overseer.h>
#include <bridgepp/BridgeUtils.h>
@ -58,7 +59,7 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
app().grpc().setLog(&log);
this->connectGrpcEvents();
app().grpc().connectToServer(serviceConfig, app().bridgeMonitor());
app().grpc().connectToServer(bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
app().log().info("Connected to backend via gRPC service.");
QString bridgeVer;
@ -178,16 +179,6 @@ void QMLBackend::setShowSplashScreen(bool show) {
}
//****************************************************************************************************************************************************
/// \return The value for the 'showSplashScreen' property.
//****************************************************************************************************************************************************
bool QMLBackend::showSplashScreen() const {
HANDLE_EXCEPTION_RETURN_BOOL(
return showSplashScreen_;
)
}
//****************************************************************************************************************************************************
/// \return The value for the 'GOOS' property.
//****************************************************************************************************************************************************
@ -198,6 +189,16 @@ QString QMLBackend::goos() const {
}
//****************************************************************************************************************************************************
/// \return The value for the 'showSplashScreen' property.
//****************************************************************************************************************************************************
bool QMLBackend::showSplashScreen() const {
HANDLE_EXCEPTION_RETURN_BOOL(
return showSplashScreen_;
)
}
//****************************************************************************************************************************************************
/// \return The value for the 'logsPath' property.
//****************************************************************************************************************************************************
@ -465,6 +466,7 @@ bool QMLBackend::isDoHEnabled() const {
)
}
//****************************************************************************************************************************************************
/// \return The value for the 'isAutomaticUpdateOn' property.
//****************************************************************************************************************************************************
@ -910,6 +912,24 @@ void QMLBackend::onUserBadEvent(QString const &userID, QString const& ) {
}
//****************************************************************************************************************************************************
/// \param[in] username The username (or primary email address)
//****************************************************************************************************************************************************
void QMLBackend::onIMAPLoginFailed(QString const &username) {
HANDLE_EXCEPTION(
SPUser const user = users_->getUserWithUsernameOrEmail(username);
if ((!user) || (user->state() != UserState::SignedOut)) { // We want to pop-up only if a signed-out user has been detected
return;
}
if (user->isInIMAPLoginFailureCooldown())
return;
user->startImapLoginFailureCooldown(60 * 60 * 1000); // 1 hour cooldown during which we will not display this notification to this user again.
emit selectUser(user->id());
emit imapLoginWhileSignedOut(username);
)
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
@ -1018,6 +1038,8 @@ void QMLBackend::connectGrpcEvents() {
// user events
connect(client, &GRPCClient::userDisconnected, this, &QMLBackend::userDisconnected);
connect(client, &GRPCClient::userBadEvent, this, &QMLBackend::onUserBadEvent);
connect(client, &GRPCClient::imapLoginFailed, this, &QMLBackend::onIMAPLoginFailed);
users_->connectGRPCEvents();
}

View File

@ -181,6 +181,7 @@ public slots: // slot for signals received from gRPC that need transformation in
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
void onLoginAlreadyLoggedIn(QString const &userID); ///< Slot for the LoginAlreadyLoggedIn gRPC event.
void onUserBadEvent(QString const& userID, QString const& errorMessage); ///< Slot for the userBadEvent gRPC event.
void onIMAPLoginFailed(QString const& username); ///< Slot the the imapLoginFailed event.
signals: // Signals received from the Go backend, to be forwarded to QML
void toggleAutostartFinished(); ///< Signal for the 'toggleAutostartFinished' gRPC stream event.
@ -234,6 +235,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
void selectUser(QString const); ///< Signal that request the given user account to be displayed.
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
void fatalError(bridgepp::Exception const& e) const; ///< Signal emitted when an fatal error occurs.

View File

@ -20,10 +20,8 @@
#include <bridgepp/BridgeUtils.h>
#include <bridgepp/Exception/Exception.h>
#include <QByteArray>
#include <QCryptographicHash>
#include <QString>
#include <QSysInfo>
using namespace bridgepp;
static constexpr const char *LoggerName = "bridge-gui";
@ -46,25 +44,54 @@ QString sentryAttachmentFilePath() {
}
//****************************************************************************************************************************************************
/// \brief Get a hash of the computer's host name
//****************************************************************************************************************************************************
QByteArray getProtectedHostname() {
QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256);
return hostname.toHex();
}
//****************************************************************************************************************************************************
/// \return The OS String used by sentry
//****************************************************************************************************************************************************
QString getApiOS() {
#if defined(Q_OS_DARWIN)
return "macos";
#elif defined(Q_OS_WINDOWS)
return "windows";
#else
return "linux";
#endif
switch (os()) {
case OS::MacOS:
return "macos";
case OS::Windows:
return "windows";
case OS::Linux:
default:
return "linux";
}
}
//****************************************************************************************************************************************************
/// \return The application version number.
//****************************************************************************************************************************************************
QString appVersion(const QString& version) {
return QString("%1-bridge@%2").arg(getApiOS()).arg(version);
return QString("%1-bridge@%2").arg(getApiOS(), version);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void initSentry() {
sentry_options_t *sentryOptions = newSentryOptions(PROJECT_DSN_SENTRY, sentryCacheDir().toStdString().c_str());
if (!QString(PROJECT_CRASHPAD_HANDLER_PATH).isEmpty())
sentry_options_set_handler_path(sentryOptions, PROJECT_CRASHPAD_HANDLER_PATH);
if (sentry_init(sentryOptions) != 0) {
QTextStream(stderr) << "Failed to initialize sentry\n";
}
setSentryReportScope();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void setSentryReportScope() {
sentry_set_tag("OS", bridgepp::goos().toUtf8());
sentry_set_tag("Client", PROJECT_FULL_NAME);
@ -76,6 +103,10 @@ void setSentryReportScope() {
sentry_set_user(user);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
sentry_options_t* newSentryOptions(const char *sentryDNS, const char *cacheDir) {
sentry_options_t *sentryOptions = sentry_options_new();
sentry_options_set_dsn(sentryOptions, sentryDNS);
@ -92,12 +123,18 @@ sentry_options_t* newSentryOptions(const char *sentryDNS, const char *cacheDir)
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message) {
auto event = sentry_value_new_message_event(level, LoggerName, message);
return sentry_capture_event(event);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
sentry_uuid_t reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception) {
auto event = sentry_value_new_message_event(level, LoggerName, message);
sentry_event_add_exception(event, sentry_value_new_exception(exceptionType, exception));

View File

@ -21,7 +21,7 @@
#include <sentry.h>
void initSentry();
void setSentryReportScope();
sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir);
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message);

View File

@ -0,0 +1,56 @@
// 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/>.
#include "Settings.h"
#include <bridgepp/BridgeUtils.h>
using namespace bridgepp;
namespace {
QString const settingsFileName = "bridge-gui.ini"; ///< The name of the settings file.
QString const keyUseSoftwareRenderer = "UseSoftwareRenderer"; ///< The key for storing the 'Use software rendering' setting.
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
Settings::Settings()
: settings_(QDir(userConfigDir()).absoluteFilePath("bridge-gui.ini"), QSettings::Format::IniFormat) {
}
//****************************************************************************************************************************************************
/// \return The value for the 'Use software renderer' setting.
//****************************************************************************************************************************************************
bool Settings::useSoftwareRenderer() const {
return settings_.value(keyUseSoftwareRenderer, false).toBool();
}
//****************************************************************************************************************************************************
/// \param[in] value The value for the 'Use software renderer' setting.
//****************************************************************************************************************************************************
void Settings::setUseSoftwareRenderer(bool value) {
settings_.setValue(keyUseSoftwareRenderer, value);
}

View File

@ -0,0 +1,47 @@
// 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/>.
#ifndef BRIDGE_GUI_SETTINGS_H
#define BRIDGE_GUI_SETTINGS_H
//****************************************************************************************************************************************************
/// \brief Application settings class
//****************************************************************************************************************************************************
class Settings {
public: // member functions.
Settings(Settings const&) = delete; ///< Disabled copy-constructor.
Settings(Settings&&) = delete; ///< Disabled assignment copy-constructor.
~Settings() = default; ///< Destructor.
Settings& operator=(Settings const&) = delete; ///< Disabled assignment operator.
Settings& operator=(Settings&&) = delete; ///< Disabled move assignment operator.
bool useSoftwareRenderer() const; ///< Get the 'Use software renderer' settings value.
void setUseSoftwareRenderer(bool value); ///< Set the 'Use software renderer' settings value.
private: // member functions.
Settings(); ///< Default constructor.
private: // data members.
QSettings settings_; ///< The settings.
friend class AppController;
};
#endif //BRIDGE_GUI_SETTINGS_H

View File

@ -38,6 +38,10 @@ void UserList::connectGRPCEvents() const {
GRPCClient &client = app().grpc();
connect(&client, &GRPCClient::userChanged, this, &UserList::onUserChanged);
connect(&client, &GRPCClient::toggleSplitModeFinished, this, &UserList::onToggleSplitModeFinished);
connect(&client, &GRPCClient::usedBytesChanged, this, &UserList::onUsedBytesChanged);
connect(&client, &GRPCClient::syncStarted, this, &UserList::onSyncStarted);
connect(&client, &GRPCClient::syncFinished, this, &UserList::onSyncFinished);
connect(&client, &GRPCClient::syncProgress, this, &UserList::onSyncProgress);
}
@ -148,6 +152,19 @@ bridgepp::SPUser UserList::getUserWithID(QString const &userID) const {
}
//****************************************************************************************************************************************************
/// \param[in] username The username or email.
/// \return The user with the given ID.
/// \return A null pointer if the user could not be found.
//****************************************************************************************************************************************************
bridgepp::SPUser UserList::getUserWithUsernameOrEmail(QString const &username) const {
QList<SPUser>::const_iterator it = std::find_if(users_.begin(), users_.end(), [username](SPUser const &user) -> bool {
return user && ((username.compare(user->username(), Qt::CaseInsensitive) == 0) || user->addresses().contains(username, Qt::CaseInsensitive));
});
return (it == users_.end()) ? nullptr : *it;
}
//****************************************************************************************************************************************************
/// \param[in] row The row.
//****************************************************************************************************************************************************
@ -223,3 +240,61 @@ void UserList::onToggleSplitModeFinished(QString const &userID) {
int UserList::count() const {
return users_.size();
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \param[in] usedBytes The used space, in bytes.
//****************************************************************************************************************************************************
void UserList::onUsedBytesChanged(QString const &userID, qint64 usedBytes) {
int const index = this->rowOfUserID(userID);
if (index < 0) {
app().log().error(QString("Received usedBytesChanged event for unknown userID %1").arg(userID));
return;
}
users_[index]->setUsedBytes(usedBytes);
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
//****************************************************************************************************************************************************
void UserList::onSyncStarted(QString const &userID) {
int const index = this->rowOfUserID(userID);
if (index < 0) {
app().log().error(QString("Received onSyncStarted event for unknown userID %1").arg(userID));
return;
}
users_[index]->setIsSyncing(true);
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
//****************************************************************************************************************************************************
void UserList::onSyncFinished(QString const &userID) {
int const index = this->rowOfUserID(userID);
if (index < 0) {
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
return;
}
users_[index]->setIsSyncing(false);
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \param[in] progress The sync progress ratio.
/// \param[in] elapsedMs The elapsed sync time in milliseconds.
/// \param[in] remainingMs The remaining sync time in milliseconds.
//****************************************************************************************************************************************************
void UserList::onSyncProgress(QString const &userID, double progress, float elapsedMs, float remainingMs) {
Q_UNUSED(elapsedMs)
Q_UNUSED(remainingMs)
int const index = this->rowOfUserID(userID);
if (index < 0) {
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
return;
}
users_[index]->setSyncProgress(progress);
}

View File

@ -44,6 +44,7 @@ public: // member functions.
void appendUser(bridgepp::SPUser const &user); ///< Add a new user.
void updateUserAtRow(int row, bridgepp::User const &user); ///< Update the user at given row.
bridgepp::SPUser getUserWithID(QString const &userID) const; ///< Retrieve the user with the given ID.
bridgepp::SPUser getUserWithUsernameOrEmail(QString const& username) const; ///< Retrieve the user with the given primary email address or username
// the userCount property.
Q_PROPERTY(int count READ count NOTIFY countChanged)
@ -59,7 +60,11 @@ public:
public slots: ///< handler for signals coming from the gRPC service
void onUserChanged(QString const &userID);
void onToggleSplitModeFinished(QString const &userID);
void onUsedBytesChanged(QString const &userID, qint64 usedBytes); ///< Slot for usedBytesChanged events.
void onSyncStarted(QString const &userID); ///< Slot for syncStarted events.
void onSyncFinished(QString const &userID); ///< Slot for syncFinished events.
void onSyncProgress(QString const &userID, double progress, float elapsedMs, float remainingMs); ///< Slot for syncFinished events.
private: // data members
QList<bridgepp::SPUser> users_; ///< The user list.
};

View File

@ -92,7 +92,7 @@ git submodule update --init --recursive $vcpkgRoot
. $cmakeExe -G "Visual Studio 17 2022" -DCMAKE_BUILD_TYPE="$buildConfig" `
-DBRIDGE_APP_FULL_NAME="$bridgeFullName" `
-DBRIDGE_VENDOR="$bridgeVendor" `
-DBRIDGE_REVISION=$REVISION_HASH `
-DBRIDGE_REVISION="$REVISION_HASH" `
-DBRIDGE_APP_VERSION="$bridgeVersion" `
-DBRIDGE_BUILD_TIME="$bridgeBuidTime" `
-DBRIDGE_DSN_SENTRY="$bridgeDsnSentry" `

View File

@ -16,20 +16,18 @@
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "Pch.h"
#include "BridgeApp.h"
#include "BuildConfig.h"
#include "CommandLine.h"
#include "LogUtils.h"
#include "QMLBackend.h"
#include "SentryUtils.h"
#include "BuildConfig.h"
#include "LogUtils.h"
#include "Settings.h"
#include <bridgepp/BridgeUtils.h>
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/FocusGRPC/FocusGRPCClient.h>
#include <bridgepp/Log/Log.h>
#include <bridgepp/ProcessMonitor.h>
#include <sentry.h>
#include <SentryUtils.h>
#ifdef Q_OS_MACOS
@ -99,38 +97,6 @@ void initQtApplication() {
}
//****************************************************************************************************************************************************
/// \return A reference to the log.
//****************************************************************************************************************************************************
Log &initLog() {
Log &log = app().log();
log.registerAsQtMessageHandler();
log.setEchoInConsole(true);
// remove old gui log files
QDir const logsDir(userLogsDir());
for (QFileInfo const fileInfo: logsDir.entryInfoList({ "gui_v*.log" }, QDir::Filter::Files)) { // entryInfolist apparently only support wildcards, not regex.
QFile(fileInfo.absoluteFilePath()).remove();
}
// create new GUI log file
QString error;
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error)) {
log.error(error);
}
log.info("bridge-gui starting");
QString const qtCompileTimeVersion = QT_VERSION_STR;
QString const qtRuntimeVersion = qVersion();
QString msg = QString("Using Qt %1").arg(qtRuntimeVersion);
if (qtRuntimeVersion != qtCompileTimeVersion) {
msg += QString(" (compiled against %1)").arg(qtCompileTimeVersion);
}
log.info(msg);
return log;
}
//****************************************************************************************************************************************************
/// \param[in] engine The QML component.
@ -150,8 +116,9 @@ QQmlComponent *createRootQmlComponent(QQmlApplicationEngine &engine) {
rootComponent->loadUrl(QUrl(qrcQmlDir + "/Bridge.qml"));
if (rootComponent->status() != QQmlComponent::Status::Ready) {
app().log().error(rootComponent->errorString());
throw Exception("Could not load QML component");
QString const &err =rootComponent->errorString();
app().log().error(err);
throw Exception("Could not load QML component", err);
}
return rootComponent;
}
@ -218,7 +185,7 @@ QUrl getApiUrl() {
/// \return true if an instance of bridge is already running.
//****************************************************************************************************************************************************
bool isBridgeRunning() {
QLockFile lockFile(QDir(userCacheDir()).absoluteFilePath(bridgeLock));
QLockFile lockFile(QDir(bridgepp::userCacheDir()).absoluteFilePath(bridgeLock));
return (!lockFile.tryLock()) && (lockFile.error() == QLockFile::LockFailedError);
}
@ -229,8 +196,21 @@ bool isBridgeRunning() {
void focusOtherInstance() {
try {
FocusGRPCClient client;
GRPCConfig sc;
QString const path = FocusGRPCClient::grpcFocusServerConfigPath(bridgepp::userConfigDir());
QFile file(path);
if (file.exists()) {
if (!sc.load(path)) {
throw Exception("The gRPC focus service configuration file is invalid.");
}
}
else {
throw Exception("Server did not provide gRPC Focus service configuration.");
}
QString error;
if (!client.connectToServer(5000, &error)) {
if (!client.connectToServer(5000, sc.port, &error)) {
throw Exception(QString("Could not connect to bridge focus service for a raise call: %1").arg(error));
}
if (!client.raise().ok()) {
@ -247,6 +227,7 @@ void focusOtherInstance() {
//****************************************************************************************************************************************************
/// \param [in] args list of arguments to pass to bridge.
/// \return bridge executable path
//****************************************************************************************************************************************************
const QString launchBridge(QStringList const &args) {
UPOverseer &overseer = app().bridgeOverseer();
@ -276,12 +257,8 @@ void closeBridgeApp() {
app().grpc().quit(); // this will cause the grpc service and the bridge app to close.
UPOverseer &overseer = app().bridgeOverseer();
if (!overseer) { // The app was run in 'attach' mode and attached to an existing instance of Bridge. We're not monitoring it.
return;
}
while (!overseer->isFinished()) {
QThread::msleep(20);
if (overseer) { // A null overseer means the app was run in 'attach' mode. We're not monitoring it.
overseer->wait(Overseer::maxTerminationWaitTimeMs);
}
}
@ -292,24 +269,23 @@ void closeBridgeApp() {
/// \return The exit code for the application.
//****************************************************************************************************************************************************
int main(int argc, char *argv[]) {
// Init sentry.
sentry_options_t *sentryOptions = newSentryOptions(PROJECT_DSN_SENTRY, sentryCacheDir().toStdString().c_str());
if (sentry_init(sentryOptions) != 0) {
std::cerr << "Failed to initialize sentry" << std::endl;
}
setSentryReportScope();
auto sentryClose = qScopeGuard([] { sentry_close(); });
// The application instance is needed to display system message boxes. As we may have to do it in the exception handler,
// application instance is create outside the try/catch clause.
if (QSysInfo::productType() != "windows") {
QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL);
QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); // must be called before instantiating the BridgeApp
}
BridgeApp guiApp(argc, argv);
try {
QString const& configDir = bridgepp::userConfigDir();
// Init sentry.
initSentry();
auto sentryClose = qScopeGuard([] { sentry_close(); });
initQtApplication();
Log &log = initLog();
@ -339,14 +315,16 @@ int main(int argc, char *argv[]) {
}
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
GRPCClient::removeServiceConfigFile();
FocusGRPCClient::removeServiceConfigFile(configDir);
GRPCClient::removeServiceConfigFile(configDir);
bridgeexec = launchBridge(cliOptions.bridgeArgs);
}
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath(configDir))));
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(configDir, cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs,
app().bridgeMonitor()));
if (!cliOptions.attach) {
GRPCClient::removeServiceConfigFile();
GRPCClient::removeServiceConfigFile(configDir);
}
// gRPC communication is established. From now on, log events will be sent to bridge via gRPC. bridge will write these to file,
@ -359,7 +337,7 @@ int main(int argc, char *argv[]) {
// The following allows to render QML content in software with a 'Rendering Hardware Interface' (OpenGL, Vulkan, Metal, Direct3D...)
// Note that it is different from the Qt::AA_UseSoftwareOpenGL attribute we use on some platforms that instruct Qt that we would like
// to use a software-only implementation of OpenGL.
QQuickWindow::setSceneGraphBackend(cliOptions.useSoftwareRenderer ? "software" : "rhi");
QQuickWindow::setSceneGraphBackend((app().settings().useSoftwareRenderer() || cliOptions.useSoftwareRenderer) ? "software" : "rhi");
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
@ -412,7 +390,7 @@ int main(int argc, char *argv[]) {
QObject::disconnect(connection);
app().grpc().stopEventStreamReader();
if (!app().backend().waitForEventStreamReaderToFinish(5000)) {
if (!app().backend().waitForEventStreamReaderToFinish(Overseer::maxTerminationWaitTimeMs)) {
log.warn("Event stream reader took too long to finish.");
}

View File

@ -29,14 +29,19 @@ Item {
property var _spacing: 12 * ProtonStyle.px
property color usedSpaceColor : {
property color progressColor : {
if (!root.enabled) return root.colorScheme.text_weak
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak
if (root.usedFraction < .50) return root.colorScheme.signal_success
if (root.usedFraction < .75) return root.colorScheme.signal_warning
if (root.user && root.user.isSyncing) return root.colorScheme.text_weak
if (root.progressRatio < .50) return root.colorScheme.signal_success
if (root.progressRatio < .75) return root.colorScheme.signal_warning
return root.colorScheme.signal_danger
}
property real usedFraction: root.user ? reasonableFraction(root.user.usedBytes, root.user.totalBytes) : 0
property real progressRatio: {
if (!root.user)
return 0
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes)
}
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
@ -171,18 +176,21 @@ Item {
case EUserState.Locked:
return qsTr("Connecting") + dotsTimer.dots
case EUserState.Connected:
return root.usedSpace
if (root.user.isSyncing)
return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots
else
return root.usedSpace
}
}
Timer { // dots animation while connecting. 1 sec cycle, roughly similar to the webmail loading page.
Timer { // dots animation while connecting & syncing.
id:dotsTimer
property string dots: ""
interval: 250;
interval: 500;
repeat: true;
running: (root.user != null) && (root.user.state === EUserState.Locked)
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
onTriggered: {
dots = dots + "."
dots += "."
if (dots.length > 3)
dots = ""
}
@ -191,7 +199,7 @@ Item {
}
}
color: root.usedSpaceColor
color: root.progressColor
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Caption
@ -202,7 +210,7 @@ Item {
Label {
colorScheme: root.colorScheme
text: root.user && root.user.state == EUserState.Connected ? " / " + root.totalSpace : ""
text: root.user && root.user.state == EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
color: root.colorScheme.text_weak
type: {
switch (root.type) {
@ -213,26 +221,27 @@ Item {
}
}
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0 }
Rectangle {
id: storage_bar
id: progress_bar
visible: root.user ? root.type == AccountDelegate.LargeView : false
width: 140 * ProtonStyle.px
height: 4 * ProtonStyle.px
radius: ProtonStyle.storage_bar_radius
radius: ProtonStyle.progress_bar_radius
color: root.colorScheme.border_weak
Rectangle {
id: storage_bar_filled
radius: ProtonStyle.storage_bar_radius
color: root.usedSpaceColor
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected) : false
id: progress_bar_filled
radius: ProtonStyle.progress_bar_radius
color: root.progressColor
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected): false
anchors {
top : parent.top
bottom : parent.bottom
left : parent.left
}
width: Math.min(1,Math.max(0.02,root.usedFraction)) * parent.width
width: Math.min(1,Math.max(0.02,root.progressRatio)) * parent.width
}
}
}

View File

@ -29,6 +29,8 @@ Item {
property var notifications
signal showSetupGuide(var user, string address)
signal closeWindow()
signal quitBridge()
RowLayout {
anchors.fill: parent
@ -107,7 +109,7 @@ Item {
Layout.topMargin: 16
Layout.bottomMargin: 9
Layout.rightMargin: 16
Layout.rightMargin: 4
horizontalPadding: 0
@ -115,6 +117,55 @@ Item {
onClicked: rightContent.showGeneralSettings()
}
Button {
id: dotMenuButton
Layout.bottomMargin: 9
Layout.maximumHeight: 36
Layout.maximumWidth: 36
Layout.minimumHeight: 36
Layout.minimumWidth: 36
Layout.preferredHeight: 36
Layout.preferredWidth: 36
Layout.rightMargin: 16
Layout.topMargin: 16
colorScheme: leftBar.colorScheme
horizontalPadding: 0
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
onClicked: {
dotMenu.open()
}
Menu {
id: dotMenu
colorScheme: root.colorScheme
modal: true
y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin
MenuItem {
colorScheme: root.colorScheme
text: qsTr("Close window")
onClicked: {
root.closeWindow()
}
}
MenuItem {
colorScheme: root.colorScheme
text: qsTr("Quit Bridge")
onClicked: {
root.quitBridge()
}
}
onClosed: {
parent.checked = false
}
onOpened: {
parent.checked = true
}
}
}
}
Item {implicitHeight:10}

View File

@ -142,6 +142,17 @@ ApplicationWindow {
onShowSetupGuide: function(user, address) {
root.showSetup(user,address)
}
onCloseWindow: {
root.close()
}
onQuitBridge: {
// If we ever want to add a confirmation dialog before quitting:
//root.notifications.askQuestion("Quit Bridge", "Insert warning message here.", "Quit", "Cancel", Backend.quit, null)
root.close()
Backend.quit()
}
}
WelcomeGuide { // 1

View File

@ -138,4 +138,9 @@ Item {
colorScheme: root.colorScheme
notification: root.notifications.genericError
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.genericQuestion
}
}

View File

@ -32,7 +32,7 @@ QtObject {
signal askResetBridge()
signal askChangeAllMailVisibility(var isVisibleNow)
signal askDeleteAccount(var user)
signal askQuestion(var title, var description, var option1, var option2, var action1, var action2)
enum Group {
Connection = 1,
Update = 2,
@ -81,7 +81,9 @@ QtObject {
root.apiCertIssue,
root.noActiveKeyForRecipient,
root.userBadEvent,
root.genericError
root.imapLoginWhileSignedOut,
root.genericError,
root.genericQuestion,
]
// Connection
@ -1143,6 +1145,34 @@ QtObject {
}
property Notification imapLoginWhileSignedOut: Notification {
title: qsTr("IMAP Login failed")
brief: title
description: "#PlaceHolderText"
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Connection
Connections {
target: Backend
function onImapLoginWhileSignedOut(username) {
root.imapLoginWhileSignedOut.description = qsTr("An email client tried to connect to the account %1, but this account is signed " +
"out. Please sign-in to continue.").arg(username)
root.imapLoginWhileSignedOut.active = true
}
}
action: [
Action {
text: qsTr("OK")
onTriggered: {
root.imapLoginWhileSignedOut.active = false
}
}
]
}
property Notification genericError: Notification {
title: "#PlaceholderText#"
description: "#PlaceholderText#"
@ -1168,4 +1198,50 @@ QtObject {
}
]
}
property Notification genericQuestion: Notification {
title: ""
brief: ""
description: ""
type: Notification.NotificationType.Warning
group: Notifications.Group.Dialogs
property var option1: ""
property var option2: ""
property variant action1: null
property variant action2: null
Connections {
target: root
function onAskQuestion(title, description, option1, option2, action1, action2) {
root.genericQuestion.title = title
root.genericQuestion.description = description
root.genericQuestion.option1 = option1
root.genericQuestion.option2 = option2
root.genericQuestion.action1 = action1
root.genericQuestion.action2 = action2
root.genericQuestion.active = true
}
}
action: [
Action {
text: root.genericQuestion.option1
onTriggered: {
root.genericQuestion.active = false
if (root.genericQuestion.action1)
root.genericQuestion.action1()
}
},
Action {
text: root.genericQuestion.option2
onTriggered: {
root.genericQuestion.active = false
if (root.genericQuestion.action2)
root.genericQuestion.action2()
}
}
]
}
}

View File

@ -26,13 +26,12 @@ T.MenuItem {
property ColorScheme colorScheme
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
width: parent.width // required. Other item overflows to the right of the menu and get clipped.
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
padding: 6
padding: 12
spacing: 6
icon.width: 24

View File

@ -362,7 +362,7 @@ QtObject {
property real banner_radius : 12 * root.px // px
property real dialog_radius : 12 * root.px // px
property real card_radius : 12 * root.px // px
property real storage_bar_radius : 3 * root.px // px
property real progress_bar_radius : 3 * root.px // px
property real tooltip_radius : 8 * root.px // px
property int heading_font_size: 28

View File

@ -156,16 +156,6 @@ QString userDataDir() {
}
//****************************************************************************************************************************************************
/// \return user logs directory used by bridge.
//****************************************************************************************************************************************************
QString userLogsDir() {
QString const path = QDir(userDataDir()).absoluteFilePath("logs");
QDir().mkpath(path);
return path;
}
//****************************************************************************************************************************************************
/// \return sentry cache directory used by bridge.
//****************************************************************************************************************************************************

View File

@ -38,7 +38,7 @@ enum class OS {
QString userConfigDir(); ///< Get the path of the user configuration folder.
QString userCacheDir(); ///< Get the path of the user cache folder.
QString userLogsDir(); ///< Get the path of the user logs folder.
QString userDataDir(); ///< Get the path of the user data folder.
QString sentryCacheDir(); ///< Get the path of the sentry cache folder.
QString goos(); ///< return the value of Go's GOOS for the current platform ("darwin", "linux" and "windows" are supported).
qint64 randN(qint64 n); ///< return a random integer in the half open range [0,n)

View File

@ -29,7 +29,6 @@ namespace {
Empty empty; ///< Empty protobuf message, re-used across calls.
qint64 const port = 1042; ///< The port for the focus service.
QString const hostname = "127.0.0.1"; ///< The hostname of the focus service.
@ -39,12 +38,43 @@ QString const hostname = "127.0.0.1"; ///< The hostname of the focus service.
namespace bridgepp {
//****************************************************************************************************************************************************
/// \return the gRPC Focus server config file name
//****************************************************************************************************************************************************
QString grpcFocusServerConfigFilename() {
return "grpcFocusServerConfig.json";
}
//****************************************************************************************************************************************************
/// \return The absolute path of the focus service config path.
//****************************************************************************************************************************************************
QString FocusGRPCClient::grpcFocusServerConfigPath(QString const &configDir) {
return QDir(configDir).absoluteFilePath(grpcFocusServerConfigFilename());
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void FocusGRPCClient::removeServiceConfigFile(QString const &configDir) {
QString const path = grpcFocusServerConfigPath(configDir);
if (!QFile(path).exists()) {
return;
}
if (!QFile().remove(path)) {
throw Exception("Could not remove gRPC focus service config file.");
}
}
//****************************************************************************************************************************************************
/// \param[in] timeoutMs The timeout for the connexion.
/// \param[in] port The gRPC server port.
/// \param[out] outError if not null and the function returns false.
/// \return true iff the connexion was successfully established.
//****************************************************************************************************************************************************
bool FocusGRPCClient::connectToServer(qint64 timeoutMs, QString *outError) {
bool FocusGRPCClient::connectToServer(qint64 timeoutMs, quint16 port, QString *outError) {
try {
QString const address = QString("%1:%2").arg(hostname).arg(port);
channel_ = grpc::CreateChannel(address.toStdString(), grpc::InsecureChannelCredentials());

View File

@ -27,10 +27,14 @@
namespace bridgepp {
//**********************************************************************************************************************
//****************************************************************************************************************************************************
/// \brief Focus GRPC client class
//**********************************************************************************************************************
//****************************************************************************************************************************************************
class FocusGRPCClient {
public: // static member functions
static void removeServiceConfigFile(QString const &configDir); ///< Delete the service config file.
static QString grpcFocusServerConfigPath(QString const &configDir); ///< Return the path of the gRPC Focus server config file.
public: // member functions.
FocusGRPCClient() = default; ///< Default constructor.
FocusGRPCClient(FocusGRPCClient const &) = delete; ///< Disabled copy-constructor.
@ -38,8 +42,8 @@ public: // member functions.
~FocusGRPCClient() = default; ///< Destructor.
FocusGRPCClient &operator=(FocusGRPCClient const &) = delete; ///< Disabled assignment operator.
FocusGRPCClient &operator=(FocusGRPCClient &&) = delete; ///< Disabled move assignment operator.
bool connectToServer(qint64 timeoutMs, QString *outError = nullptr); ///< Connect to the focus server
bool connectToServer(qint64 timeoutMs, quint16 port, QString *outError = nullptr); ///< Connect to the focus server
grpc::Status raise(); ///< Performs the 'raise' call.
grpc::Status version(QString &outVersion); ///< Performs the 'version' call.

View File

@ -574,6 +574,78 @@ SPStreamEvent newUserBadEvent(QString const &userID, QString const &errorMessage
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \param[in] usedBytes The number of used bytes.
//****************************************************************************************************************************************************
SPStreamEvent newUsedBytesChangedEvent(QString const &userID, qint64 usedBytes) {
auto event = new grpc::UsedBytesChangedEvent;
event->set_userid(userID.toStdString());
event->set_usedbytes(usedBytes);
auto userEvent = new grpc::UserEvent;
userEvent->set_allocated_usedbyteschangedevent(event);
return wrapUserEvent(userEvent);
}
//****************************************************************************************************************************************************
/// \param[in] username The username that was provided for the failed IMAP login attempt.
/// \return The event.
//****************************************************************************************************************************************************
SPStreamEvent newIMAPLoginFailedEvent(QString const &username) {
auto event = new grpc::ImapLoginFailedEvent;
event->set_username(username.toStdString());
auto userEvent = new grpc::UserEvent;
userEvent->set_allocated_imaploginfailedevent(event);
return wrapUserEvent(userEvent);
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \return The event.
//****************************************************************************************************************************************************
SPStreamEvent newSyncStartedEvent(QString const &userID) {
auto event = new grpc::SyncStartedEvent;
event->set_userid(userID.toStdString());
auto userEvent = new grpc::UserEvent;
userEvent->set_allocated_syncstartedevent(event);
return wrapUserEvent(userEvent);
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \return The event.
//****************************************************************************************************************************************************
SPStreamEvent newSyncFinishedEvent(QString const &userID) {
auto event = new grpc::SyncFinishedEvent;
event->set_userid(userID.toStdString());
auto userEvent = new grpc::UserEvent;
userEvent->set_allocated_syncfinishedevent(event);
return wrapUserEvent(userEvent);
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \param[in] progress The progress ratio.
/// \param[in] elapsedMs The elapsed time in milliseconds.
/// \param[in] remainingMs The remaining time in milliseconds.
/// \return The event.
//****************************************************************************************************************************************************
SPStreamEvent newSyncProgressEvent(QString const &userID, double progress, qint64 elapsedMs, qint64 remainingMs) {
auto event = new grpc::SyncProgressEvent;
event->set_userid(userID.toStdString());
event->set_progress(progress);
event->set_elapsedms(elapsedMs);
event->set_remainingms(remainingMs);
auto userEvent = new grpc::UserEvent;
userEvent->set_allocated_syncprogressevent(event);
return wrapUserEvent(userEvent);
}
//****************************************************************************************************************************************************
/// \param[in] errorCode The error errorCode.
/// \return The event.

View File

@ -78,6 +78,11 @@ SPStreamEvent newToggleSplitModeFinishedEvent(QString const &userID); ///< Creat
SPStreamEvent newUserDisconnectedEvent(QString const &username); ///< Create a new UserDisconnectedEvent event.
SPStreamEvent newUserChangedEvent(QString const &userID); ///< Create a new UserChangedEvent event.
SPStreamEvent newUserBadEvent(QString const &userID, QString const& errorMessage); ///< Create a new UserBadEvent event.
SPStreamEvent newUsedBytesChangedEvent(QString const &userID, qint64 usedBytes); ///< Create a new UsedBytesChangedEvent event.
SPStreamEvent newIMAPLoginFailedEvent(QString const &username); ///< Create a new ImapLoginFailedEvent event.
SPStreamEvent newSyncStartedEvent(QString const &userID); ///< Create a new SyncStarted event.
SPStreamEvent newSyncFinishedEvent(QString const &userID); ///< Create a new SyncFinished event.
SPStreamEvent newSyncProgressEvent(QString const &userID, double progress, qint64 elapsedMs, qint64 remainingMs); ///< Create a new SyncFinished event.
// Generic error event
SPStreamEvent newGenericErrorEvent(grpc::ErrorCode errorCode); ///< Create a new GenericErrrorEvent event.

View File

@ -45,8 +45,8 @@ qint64 const grpcConnectionRetryDelayMs = 10000; ///< Retry delay for the gRPC c
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void GRPCClient::removeServiceConfigFile() {
QString const path = grpcServerConfigPath();
void GRPCClient::removeServiceConfigFile(QString const &configDir) {
QString const path = grpcServerConfigPath(configDir);
if (!QFile(path).exists()) {
return;
}
@ -61,8 +61,8 @@ void GRPCClient::removeServiceConfigFile() {
/// \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 The service config.
//****************************************************************************************************************************************************
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMonitor *serverProcess) {
QString const path = grpcServerConfigPath();
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const &configDir, qint64 timeoutMs, ProcessMonitor *serverProcess) {
QString const path = grpcServerConfigPath(configDir);
QFile file(path);
QElapsedTimer timer;
@ -109,7 +109,7 @@ void GRPCClient::setLog(Log *log) {
/// \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.
//****************************************************************************************************************************************************
void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess) {
void GRPCClient::connectToServer(QString const &configDir, GRPCConfig const &config, ProcessMonitor *serverProcess) {
try {
serverToken_ = config.token.toStdString();
QString address;
@ -147,8 +147,9 @@ void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
break;
} // connection established.
if (QDateTime::currentDateTime() > giveUpTime)
if (QDateTime::currentDateTime() > giveUpTime) {
throw Exception("Connection to the RPC server failed.");
}
}
if (channel_->GetState(true) != GRPC_CHANNEL_READY) {
@ -159,7 +160,7 @@ void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
QString const clientToken = QUuid::createUuid().toString();
QString error;
QString clientConfigPath = createClientConfigFile(clientToken, &error);
QString clientConfigPath = createClientConfigFile(configDir, clientToken, &error);
if (clientConfigPath.isEmpty()) {
throw Exception("gRPC client config could not be saved.", error);
}
@ -184,6 +185,14 @@ void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
}
//****************************************************************************************************************************************************
/// \return true if the gRPC client is connected to the server.
//****************************************************************************************************************************************************
bool GRPCClient::isConnected() const {
return stub_.get();
}
//****************************************************************************************************************************************************
/// \param[in] clientConfigPath The path to the gRPC client config path.-
/// \param[in] serverToken The token obtained from the server config file.
@ -223,8 +232,9 @@ grpc::Status GRPCClient::addLogEntry(Log::Level level, QString const &package, Q
grpc::Status GRPCClient::guiReady(bool &outShowSplashScreen) {
GuiReadyResponse response;
Status status = this->logGRPCCallStatus(stub_->GuiReady(this->clientContext().get(), empty, &response), __FUNCTION__);
if (status.ok())
if (status.ok()) {
outShowSplashScreen = response.showsplashscreen();
}
return status;
}
@ -395,6 +405,8 @@ grpc::Status GRPCClient::setIsDoHEnabled(bool enabled) {
//****************************************************************************************************************************************************
grpc::Status GRPCClient::quit() {
// quitting will shut down the gRPC service, to we may get an 'Unavailable' response for the call
if (!this->isConnected())
return Status::OK; // We're not even connected, we return OK. This maybe be an attempt to do 'a proper' shutdown after an unrecoverable error.
return this->logGRPCCallStatus(stub_->Quit(this->clientContext().get(), empty, &empty), __FUNCTION__, { StatusCode::UNAVAILABLE });
}
@ -458,15 +470,6 @@ grpc::Status GRPCClient::showOnStartup(bool &outValue) {
}
//****************************************************************************************************************************************************
/// \param[out] outGoos The value for the property.
/// \return The status for the gRPC call.
//****************************************************************************************************************************************************
grpc::Status GRPCClient::goos(QString &outGoos) {
return this->logGRPCCallStatus(this->getString(&Bridge::Stub::GoOs, outGoos), __FUNCTION__);
}
//****************************************************************************************************************************************************
/// \param[out] outPath The value for the property.
/// \return The status for the gRPC call.
@ -476,6 +479,15 @@ grpc::Status GRPCClient::logsPath(QUrl &outPath) {
}
//****************************************************************************************************************************************************
/// \param[out] outGoos The value for the property.
/// \return The status for the gRPC call.
//****************************************************************************************************************************************************
grpc::Status GRPCClient::goos(QString &outGoos) {
return this->logGRPCCallStatus(this->getString(&Bridge::Stub::GoOs, outGoos), __FUNCTION__);
}
//****************************************************************************************************************************************************
/// \param[out] outPath The value for the property.
/// \return The status for the gRPC call.
@ -1377,13 +1389,50 @@ void GRPCClient::processUserEvent(UserEvent const &event) {
break;
}
case UserEvent::kUserBadEvent: {
UserBadEvent const& e = event.userbadevent();
UserBadEvent const &e = event.userbadevent();
QString const userID = QString::fromStdString(e.userid());
QString const errorMessage = QString::fromStdString(e.errormessage());
this->logTrace(QString("User event received: UserBadEvent (userID = %1, errorMessage = %2).").arg(userID, errorMessage));
emit userBadEvent(userID, errorMessage);
break;
}
case UserEvent::kUsedBytesChangedEvent: {
UsedBytesChangedEvent const &e = event.usedbyteschangedevent();
QString const userID = QString::fromStdString(e.userid());
qint64 const usedBytes = e.usedbytes();
this->logTrace(QString("User event received: UsedBytesChangedEvent (userID = %1, usedBytes = %2).").arg(userID).arg(usedBytes));
emit usedBytesChanged(userID, usedBytes);
break;
}
case UserEvent::kImapLoginFailedEvent: {
ImapLoginFailedEvent const &e = event.imaploginfailedevent();
QString const username = QString::fromStdString(e.username());
this->logTrace(QString("User event received: IMAPLoginFailed (username = %1).:").arg(username));
emit imapLoginFailed(username);
break;
}
case UserEvent::kSyncStartedEvent: {
SyncStartedEvent const &e = event.syncstartedevent();
QString const &userID = QString::fromStdString(e.userid());
this->logTrace(QString("User event received: SyncStarted (userID = %1).:").arg(userID));
emit syncStarted(userID);
break;
}
case UserEvent::kSyncFinishedEvent: {
SyncFinishedEvent const &e = event.syncfinishedevent();
QString const &userID = QString::fromStdString(e.userid());
this->logTrace(QString("User event received: SyncFinished (userID = %1).:").arg(userID));
emit syncFinished(userID);
break;
}
case UserEvent::kSyncProgressEvent: {
SyncProgressEvent const &e = event.syncprogressevent();
QString const &userID = QString::fromStdString(e.userid());
this->logTrace(QString("User event received SyncProgress (userID = %1, progress = %2, elapsedMs = %3, remainingMs = %4).").arg(userID)
.arg(e.progress()).arg(e.elapsedms()).arg(e.remainingms()));
emit syncProgress(userID, e.progress(), e.elapsedms(), e.remainingms());
break;
}
default:
this->logError("Unknown User event received.");
}

View File

@ -48,8 +48,8 @@ typedef std::unique_ptr<grpc::ClientContext> UPClientContext;
class GRPCClient : public QObject {
Q_OBJECT
public: // static member functions
static void removeServiceConfigFile(); ///< Delete the service config file.
static GRPCConfig waitAndRetrieveServiceConfig(qint64 timeoutMs, class ProcessMonitor *serverProcess); ///< Wait and retrieve the service configuration.
static void removeServiceConfigFile(QString const &configDir); ///< Delete the service config file.
static GRPCConfig waitAndRetrieveServiceConfig(QString const &configDir, qint64 timeoutMs, class ProcessMonitor *serverProcess); ///< Wait and retrieve the service configuration.
public: // member functions.
GRPCClient() = default; ///< Default constructor.
@ -59,7 +59,8 @@ public: // member functions.
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
void setLog(Log *log); ///< Set the log for the client.
void connectToServer(GRPCConfig const &config, class ProcessMonitor *serverProcess); ///< Establish connection to the gRPC server.
void connectToServer(QString const &configDir, GRPCConfig const &config, class ProcessMonitor *serverProcess); ///< Establish connection to the gRPC server.
bool isConnected() const; ///< Check whether the gRPC client is connected to the server.
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.
@ -180,6 +181,11 @@ signals:
void userDisconnected(QString const &username);
void userChanged(QString const &userID);
void userBadEvent(QString const &userID, QString const& errorMessage);
void usedBytesChanged(QString const &userID, qint64 usedBytes);
void imapLoginFailed(QString const& username);
void syncStarted(QString const &userID);
void syncFinished(QString const &userID);
void syncProgress(QString const &userID, double progress, qint64 elapsedMs, qint64 remainingMs);
public: // keychain related calls
grpc::Status availableKeychains(QStringList &outKeychains);

View File

@ -59,18 +59,19 @@ bool useFileSocketForGRPC() {
//****************************************************************************************************************************************************
/// \param[in] configDir The folder containing the configuration files.
/// \return The absolute path of the service config path.
//****************************************************************************************************************************************************
QString grpcServerConfigPath() {
return QDir(userConfigDir()).absoluteFilePath(grpcServerConfigFilename());
QString grpcServerConfigPath(QString const &configDir) {
return QDir(configDir).absoluteFilePath(grpcServerConfigFilename());
}
//****************************************************************************************************************************************************
/// \return The absolute path of the service config path.
//****************************************************************************************************************************************************
QString grpcClientConfigBasePath() {
return QDir(userConfigDir()).absoluteFilePath(grpcClientConfigBaseFilename());
QString grpcClientConfigBasePath(QString const &configDir) {
return QDir(configDir).absoluteFilePath(grpcClientConfigBaseFilename());
}
@ -81,8 +82,8 @@ QString grpcClientConfigBasePath() {
/// \return The path of the created file.
/// \return A null string if the file could not be saved.
//****************************************************************************************************************************************************
QString createClientConfigFile(QString const &token, QString *outError) {
QString const basePath = grpcClientConfigBasePath();
QString createClientConfigFile(QString const &configDir, QString const &token, QString *outError) {
QString const basePath = grpcClientConfigBasePath(configDir);
QString path, error;
for (qint32 i = 0; i < 1000; ++i) // we try a decent amount of times
{
@ -255,4 +256,4 @@ QString getAvailableFileSocketPath() {
}
} // namespace bridgepp
} // namespace bridgepp

View File

@ -34,9 +34,9 @@ extern std::string const grpcMetadataServerTokenKey; ///< The key for the server
typedef std::shared_ptr<grpc::StreamEvent> SPStreamEvent; ///< Type definition for shared pointer to grpc::StreamEvent.
QString grpcServerConfigPath(); ///< Return the path of the gRPC server config file.
QString grpcClientConfigBasePath(); ///< Return the path of the gRPC client config file.
QString createClientConfigFile(QString const &token, QString *outError); ///< Create the client config file the server will retrieve and return its path.
QString grpcServerConfigPath(QString const &configDir); ///< Return the path of the gRPC server config file.
QString grpcClientConfigBasePath(QString const &configDir); ///< Return the path of the gRPC client config file.
QString createClientConfigFile(QString const &configDir, 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.
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.

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,8 @@ SPUser User::newUser(QObject *parent) {
/// \param[in] parent The parent object.
//****************************************************************************************************************************************************
User::User(QObject *parent)
: QObject(parent) {
: QObject(parent)
, imapFailureCooldownEndTime_(QDateTime::currentDateTime()) {
}
@ -293,6 +294,48 @@ void User::setTotalBytes(float totalBytes) {
}
//****************************************************************************************************************************************************
/// \return true iff a sync is in progress.
//****************************************************************************************************************************************************
bool User::isSyncing() const {
return isSyncing_;
}
//****************************************************************************************************************************************************
/// \param[in] syncing The new value for the sync state.
//****************************************************************************************************************************************************
void User::setIsSyncing(bool syncing) {
if (isSyncing_ == syncing) {
return;
}
isSyncing_ = syncing;
emit isSyncingChanged(syncing);
}
//****************************************************************************************************************************************************
/// \return The sync progress ratio
//****************************************************************************************************************************************************
float User::syncProgress() const {
return syncProgress_;
}
//****************************************************************************************************************************************************
/// \param[in] progress The progress ratio.
//****************************************************************************************************************************************************
void User::setSyncProgress(float progress) {
if (qAbs(syncProgress_ - progress) < 0.00001) {
return;
}
syncProgress_ = progress;
emit syncProgressChanged(progress);
}
//****************************************************************************************************************************************************
/// \param[in] state The user state.
/// \return A string describing the state.
@ -311,4 +354,24 @@ QString User::stateToString(UserState state) {
}
//****************************************************************************************************************************************************
/// We display a notification and pop the application window if an IMAP client tries to connect to a signed out account, but we do not want to
/// do it repeatedly, as it's an intrusive action. This function let's you define a period of time during which the notification should not be
/// displayed.
///
/// \param durationMSecs The duration of the period in milliseconds.
//****************************************************************************************************************************************************
void User::startImapLoginFailureCooldown(qint64 durationMSecs) {
imapFailureCooldownEndTime_ = QDateTime::currentDateTime().addMSecs(durationMSecs);
}
//****************************************************************************************************************************************************
/// \return true if we currently are in a cooldown period for the notification
//****************************************************************************************************************************************************
bool User::isInIMAPLoginFailureCooldown() const {
return QDateTime::currentDateTime() < imapFailureCooldownEndTime_;
}
} // namespace bridgepp

View File

@ -74,6 +74,8 @@ public: // member functions.
User &operator=(User &&) = delete; ///< Disabled move assignment operator.
void update(User const &user); ///< Update the user.
Q_INVOKABLE QString primaryEmailOrUsername() const; ///< Return the user primary email, or, if unknown its username.
void startImapLoginFailureCooldown(qint64 durationMSecs); ///< Start the user cooldown period for the IMAP login attempt while signed-out notification.
bool isInIMAPLoginFailureCooldown() const; ///< Check if the user in a IMAP login failure notification.
public slots:
// slots for QML generated calls
@ -99,6 +101,8 @@ public:
Q_PROPERTY(bool splitMode READ splitMode WRITE setSplitMode NOTIFY splitModeChanged)
Q_PROPERTY(float usedBytes READ usedBytes WRITE setUsedBytes NOTIFY usedBytesChanged)
Q_PROPERTY(float totalBytes READ totalBytes WRITE setTotalBytes NOTIFY totalBytesChanged)
Q_PROPERTY(bool isSyncing READ isSyncing WRITE setIsSyncing NOTIFY isSyncingChanged)
Q_PROPERTY(float syncProgress READ syncProgress WRITE setSyncProgress NOTIFY syncProgressChanged)
QString id() const;
void setID(QString const &id);
@ -118,6 +122,10 @@ public:
void setUsedBytes(float usedBytes);
float totalBytes() const;
void setTotalBytes(float totalBytes);
bool isSyncing() const;
void setIsSyncing(bool syncing);
float syncProgress() const;
void setSyncProgress(float progress);
signals:
// signals used for Qt properties
@ -132,11 +140,14 @@ signals:
void usedBytesChanged(float byteCount);
void totalBytesChanged(float byteCount);
void toggleSplitModeFinished();
void isSyncingChanged(bool syncing);
void syncProgressChanged(float syncProgress);
private: // member functions.
User(QObject *parent); ///< Default constructor.
private: // data members.
QDateTime imapFailureCooldownEndTime_; ///< The end date/time for the IMAP login failure notification cooldown period.
QString id_; ///< The userID.
QString username_; ///< The username
QString password_; ///< The IMAP password of the user.
@ -146,6 +157,8 @@ private: // data members.
bool splitMode_ { false }; ///< Is split mode active.
float usedBytes_ { 0.0f }; ///< The storage used by the user.
float totalBytes_ { 1.0f }; ///< The storage quota of the user.
bool isSyncing_ { false }; ///< Is a sync in progress for the user.
float syncProgress_ { 0.0f }; ///< The sync progress.
};

View File

@ -84,7 +84,8 @@ void Overseer::releaseWorker() {
if (thread_) {
if (!thread_->isFinished()) {
thread_->quit();
thread_->wait();
if (!thread_->wait(maxTerminationWaitTimeMs))
thread_->terminate();
}
thread_->deleteLater();
thread_ = nullptr;

View File

@ -46,6 +46,9 @@ public slots:
void startWorker(bool autorelease) const; ///< Run the worker.
void releaseWorker(); ///< Delete the worker and its thread.
public: // static data members
static qint64 const maxTerminationWaitTimeMs { 10000 }; ///< The maximum wait time for the termination of a thread
public: // data members.
QThread *thread_ { nullptr }; ///< The thread.
Worker *worker_ { nullptr }; ///< The worker.