feat(BRIDGE-75): Bridge repair button/feature implemented

This commit is contained in:
Atanas Janeshliev
2024-06-03 12:37:23 +00:00
parent e2b361b9a6
commit ff0615167b
23 changed files with 1502 additions and 1040 deletions

View File

@ -1328,6 +1328,8 @@ void QMLBackend::connectGrpcEvents() {
connect(client, &GRPCClient::certificateInstallFailed, this, &QMLBackend::certificateInstallFailed);
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
connect(client, &GRPCClient::knowledgeBasSuggestionsReceived, this, &QMLBackend::receivedKnowledgeBaseSuggestions);
connect(client, &GRPCClient::repairStarted, this, &QMLBackend::repairStarted);
connect(client, &GRPCClient::allUsersLoaded, this, &QMLBackend::allUsersLoaded);
// cache events
connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache);
@ -1410,3 +1412,9 @@ void QMLBackend::displayBadEventDialog(QString const &userID) {
emit showMainWindow();
)
}
void QMLBackend::triggerRepair() const {
HANDLE_EXCEPTION(
app().grpc().triggerRepair();
)
}

View File

@ -208,6 +208,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
void notifyExternalLinkClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
void triggerRepair() const; ///< Slot for the triggering of the bridge repair function i.e. 'resync'.
public slots: // slots for functions that need to be processed locally.
void setNormalTrayIcon(); ///< Set the tray icon to normal.
@ -282,7 +283,9 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
void receivedKnowledgeBaseSuggestions(QList<bridgepp::KnowledgeBaseSuggestion> const& suggestions); ///< Signal for the reception of knowledgebase article suggestions.
void receivedKnowledgeBaseSuggestions(QList<bridgepp::KnowledgeBaseSuggestion> const& suggestions); ///< Signal for the reception of knowledge base article suggestions.
void repairStarted(); ///< Signal for the 'repairStarted' gRPC stream event.
void allUsersLoaded(); ///< Signal for the 'allUsersLoaded' gRPC stream event
// 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

@ -21,6 +21,7 @@ SettingsView {
property bool _isAdvancedShown: false
property var notifications
property var allUsersLoaded: false
fillHeight: false
@ -219,6 +220,28 @@ SettingsView {
Backend.exportTLSCertificates();
}
}
SettingsItem {
id: repair
Layout.fillWidth: true
actionText: qsTr("Repair")
colorScheme: root.colorScheme
description: qsTr("Reload all accounts, cached data, and download all emails again. Email clients stay connected to Bridge.")
text: qsTr("Repair Bridge")
type: SettingsItem.Button
visible: root._isAdvancedShown
enabled: root.allUsersLoaded && Backend.users.count
onClicked: {
root.notifications.askRepairBridge();
}
Connections {
function onAllUsersLoaded() {
root.allUsersLoaded = true;
}
target: Backend
}
}
SettingsItem {
id: reset
Layout.fillWidth: true

View File

@ -105,4 +105,8 @@ Item {
colorScheme: root.colorScheme
notification: root.notifications.genericQuestion
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.repairBridge
}
}

View File

@ -13,6 +13,8 @@
import QtQml
import Qt.labs.platform
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick
import "../"
QtObject {
@ -60,7 +62,7 @@ QtObject {
target: Backend
}
}
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion, root.hvErrorEvent]
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion, root.hvErrorEvent, root.repairBridge]
property Notification alreadyLoggedIn: Notification {
brief: qsTr("Already signed in")
description: qsTr("This account is already signed in.")
@ -1150,6 +1152,52 @@ QtObject {
target: Backend
}
}
property Notification repairBridge: Notification {
brief: title
description: qsTr("This action will reload all accounts, cached data, and re-download emails. Messages may temporarily disappear but will reappear progressively. Email clients stay connected to Bridge.")
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg"
title: qsTr("Repair Bridge?")
type: Notification.NotificationType.Danger
action: [
Action {
id: repairBridge_cancel
text: qsTr("Cancel")
onTriggered: {
root.repairBridge.active = false;
}
},
Action {
id: repairBridge_repair
text: qsTr("Repair")
onTriggered: {
repairBridge_repair.loading = true;
repairBridge_repair.enabled = false;
repairBridge_cancel.enabled = false;
Backend.triggerRepair();
}
}
]
Connections {
function onAskRepairBridge() {
root.repairBridge.active = true;
}
target: root
}
Connections {
function onRepairStarted() {
root.repairBridge.active = false;
repairBridge_repair.loading = false;
repairBridge_repair.enabled = true;
repairBridge_cancel.enabled = true;
}
target: Backend
}
}
signal askChangeAllMailVisibility(var isVisibleNow)
@ -1158,4 +1206,5 @@ QtObject {
signal askEnableSplitMode(var user)
signal askQuestion(var title, var description, var option1, var option2, var action1, var action2)
signal askResetBridge
signal askRepairBridge
}

View File

@ -1198,6 +1198,14 @@ void GRPCClient::processAppEvent(AppEvent const &event) {
emit knowledgeBasSuggestionsReceived(suggestions);
break;
}
case AppEvent::kRepairStarted:
this->logTrace("App event received: RepairStarted.");
emit repairStarted();
break;
case AppEvent::kAllUsersLoaded:
this->logTrace("App event received: AllUsersLoaded");
emit allUsersLoaded();
break;
default:
this->logError("Unknown App event received.");
}
@ -1580,5 +1588,12 @@ grpc::Status GRPCClient::externalLinkClicked(QString const &link) {
return this->logGRPCCallStatus(stub_->ExternalLinkClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
grpc::Status GRPCClient::triggerRepair() {
return this->logGRPCCallStatus(stub_->TriggerRepair(this->clientContext().get(), empty, &empty), __FUNCTION__ );
}
} // namespace bridgepp

View File

@ -108,6 +108,7 @@ public: // member functions.
grpc::Status landingPageLink(QUrl &outUrl); ///< Performs the 'landingPageLink' call.
grpc::Status hostname(QString &outHostname); ///< Performs the 'Hostname' call.
grpc::Status requestKnowledgeBaseSuggestions(QString const &input); ///< Performs the 'RequestKnowledgeBaseSuggestions' call.
grpc::Status triggerRepair(); ///< Performs the triggerRepair gRPC call.
signals: // app related signals
void internetStatus(bool isOn);
@ -122,6 +123,8 @@ signals: // app related signals
void certificateInstallFailed();
void showMainWindow();
void knowledgeBasSuggestionsReceived(QList<KnowledgeBaseSuggestion> const& suggestions);
void repairStarted();
void allUsersLoaded();
public: // cache related calls

View File

@ -306,6 +306,12 @@ func New(
Aliases: []string{"del", "rm", "remove"},
Completer: fe.completeUsernames,
})
fe.AddCmd(&ishell.Cmd{
Name: "repair",
Help: "reload all accounts and cached data, re-download emails. Email clients remain connected. Logged out users will be repaired on next login. (aliases: rep)",
Func: fe.repair,
Aliases: []string{"rep"},
})
badEventCmd := &ishell.Cmd{
Name: "bad-event",

View File

@ -359,3 +359,9 @@ func (f *frontendCLI) isFile(location string) bool {
return !stat.IsDir()
}
func (f *frontendCLI) repair(_ *ishell.Context) {
if f.yesNoQuestion("Are you sure you want to initialize a repair, this may take a while") {
f.bridge.Repair()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -111,6 +111,9 @@ service Bridge {
// Server -> Client event stream
rpc RunEventStream(EventStreamRequest) returns (stream StreamEvent); // Keep streaming until StopEventStream is called.
rpc StopEventStream(google.protobuf.Empty) returns (google.protobuf.Empty);
// Repair
rpc TriggerRepair(google.protobuf.Empty) returns (google.protobuf.Empty);
}
//**********************************************************************************************************************
@ -272,6 +275,8 @@ message AppEvent {
CertificateInstallCanceledEvent certificateInstallCanceled = 10;
CertificateInstallFailedEvent certificateInstallFailed = 11;
KnowledgeBaseSuggestionsEvent knowledgeBaseSuggestions = 12;
RepairStartedEvent repairStarted = 13;
AllUsersLoadedEvent allUsersLoaded = 14;
}
}
@ -289,6 +294,8 @@ message ReportBugFallbackEvent {}
message CertificateInstallSuccessEvent {}
message CertificateInstallCanceledEvent {}
message CertificateInstallFailedEvent {}
message RepairStartedEvent {}
message AllUsersLoadedEvent {}
message KnowledgeBaseSuggestion {
string url = 1;

View File

@ -101,6 +101,7 @@ const (
Bridge_ExportTLSCertificates_FullMethodName = "/grpc.Bridge/ExportTLSCertificates"
Bridge_RunEventStream_FullMethodName = "/grpc.Bridge/RunEventStream"
Bridge_StopEventStream_FullMethodName = "/grpc.Bridge/StopEventStream"
Bridge_TriggerRepair_FullMethodName = "/grpc.Bridge/TriggerRepair"
)
// BridgeClient is the client API for Bridge service.
@ -180,6 +181,8 @@ type BridgeClient interface {
// Server -> Client event stream
RunEventStream(ctx context.Context, in *EventStreamRequest, opts ...grpc.CallOption) (Bridge_RunEventStreamClient, error)
StopEventStream(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Repair
TriggerRepair(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type bridgeClient struct {
@ -780,6 +783,15 @@ func (c *bridgeClient) StopEventStream(ctx context.Context, in *emptypb.Empty, o
return out, nil
}
func (c *bridgeClient) TriggerRepair(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Bridge_TriggerRepair_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// BridgeServer is the server API for Bridge service.
// All implementations must embed UnimplementedBridgeServer
// for forward compatibility
@ -857,6 +869,8 @@ type BridgeServer interface {
// Server -> Client event stream
RunEventStream(*EventStreamRequest, Bridge_RunEventStreamServer) error
StopEventStream(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
// Repair
TriggerRepair(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
mustEmbedUnimplementedBridgeServer()
}
@ -1053,6 +1067,9 @@ func (UnimplementedBridgeServer) RunEventStream(*EventStreamRequest, Bridge_RunE
func (UnimplementedBridgeServer) StopEventStream(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method StopEventStream not implemented")
}
func (UnimplementedBridgeServer) TriggerRepair(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method TriggerRepair not implemented")
}
func (UnimplementedBridgeServer) mustEmbedUnimplementedBridgeServer() {}
// UnsafeBridgeServer may be embedded to opt out of forward compatibility for this service.
@ -2203,6 +2220,24 @@ func _Bridge_StopEventStream_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler)
}
func _Bridge_TriggerRepair_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BridgeServer).TriggerRepair(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Bridge_TriggerRepair_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BridgeServer).TriggerRepair(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
// Bridge_ServiceDesc is the grpc.ServiceDesc for Bridge service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -2458,6 +2493,10 @@ var Bridge_ServiceDesc = grpc.ServiceDesc{
MethodName: "StopEventStream",
Handler: _Bridge_StopEventStream_Handler,
},
{
MethodName: "TriggerRepair",
Handler: _Bridge_TriggerRepair_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View File

@ -241,6 +241,14 @@ func NewGenericErrorEvent(errorCode ErrorCode) *StreamEvent {
return genericErrorEvent(&GenericErrorEvent{Code: errorCode})
}
func NewRepairStartedEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_RepairStarted{RepairStarted: &RepairStartedEvent{}}})
}
func NewAllUsersLoadedEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_AllUsersLoaded{AllUsersLoaded: &AllUsersLoadedEvent{}}})
}
// Event category factory functions.
func appEvent(appEvent *AppEvent) *StreamEvent {

View File

@ -401,6 +401,9 @@ func (s *Service) watchEvents() {
case events.TLSIssue:
_ = s.SendEvent(NewMailApiCertIssue())
case events.AllUsersLoaded:
_ = s.SendEvent(NewAllUsersLoadedEvent())
}
}
}

View File

@ -838,6 +838,20 @@ func (s *Service) CurrentKeychain(_ context.Context, _ *emptypb.Empty) (*wrapper
return wrapperspb.String(helper), nil
}
func (s *Service) TriggerRepair(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Debug("TriggerRepair")
go func() {
defer func() {
async.HandlePanic(s.panicHandler)
_ = s.SendEvent(NewRepairStartedEvent())
}()
s.bridge.Repair()
}()
return &emptypb.Empty{}, nil
}
func base64Decode(in []byte) ([]byte, error) {
out := make([]byte, base64.StdEncoding.DecodedLen(len(in)))