GODT-1913: pass reporter to gluon, limit restarts, add crash handlers.

This commit is contained in:
Jakub
2022-10-21 18:41:31 +02:00
committed by James Houlahan
parent 31fb878bbd
commit ae87d7b236
28 changed files with 811 additions and 34 deletions

View File

@ -30,6 +30,7 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/watcher"
"github.com/ProtonMail/proton-bridge/v2/internal/async"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
@ -39,7 +40,6 @@ import (
"github.com/ProtonMail/proton-bridge/v2/internal/user"
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
"github.com/bradenaw/juniper/xslices"
"github.com/bradenaw/juniper/xsync"
"github.com/emersion/go-smtp"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
@ -90,6 +90,12 @@ type Bridge struct {
// locator is the bridge's locator.
locator Locator
// crashHandler
crashHandler async.PanicHandler
// reporter
reporter reporter.Reporter
// watchers holds all registered event watchers.
watchers []*watcher.Watcher[events.Event]
watchersLock sync.RWMutex
@ -103,7 +109,7 @@ type Bridge struct {
logSMTP bool
// tasks manages the bridge's goroutines.
tasks *xsync.Group
tasks *async.Group
// goLoad triggers a load of disconnected users from the vault.
goLoad func()
@ -126,6 +132,8 @@ func New( //nolint:funlen
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
roundTripper http.RoundTripper, // the round tripper to use for API requests
proxyCtl ProxyController, // the DoH controller
crashHandler async.PanicHandler,
reporter reporter.Reporter,
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
logSMTP bool, // whether to log SMTP activity
@ -141,7 +149,7 @@ func New( //nolint:funlen
)
// tasks holds all the bridge's background tasks.
tasks := xsync.NewGroup(context.Background())
tasks := async.NewGroup(context.Background(), crashHandler)
// imapEventCh forwards IMAP events from gluon instances to the bridge for processing.
imapEventCh := make(chan imapEvents.Event)
@ -156,6 +164,8 @@ func New( //nolint:funlen
autostarter,
updater,
curVersion,
crashHandler,
reporter,
api,
identifier,
@ -189,7 +199,7 @@ func New( //nolint:funlen
// nolint:funlen
func newBridge(
tasks *xsync.Group,
tasks *async.Group,
imapEventCh chan imapEvents.Event,
locator Locator,
@ -197,6 +207,8 @@ func newBridge(
autostarter Autostarter,
updater Updater,
curVersion *semver.Version,
crashHandler async.PanicHandler,
reporter reporter.Reporter,
api *liteapi.Manager,
identifier Identifier,
@ -218,6 +230,7 @@ func newBridge(
gluonDir,
curVersion,
tlsConfig,
reporter,
logIMAPClient,
logIMAPServer,
imapEventCh,
@ -253,6 +266,9 @@ func newBridge(
newVersion: curVersion,
newVersionLock: safe.NewRWMutex(),
crashHandler: crashHandler,
reporter: reporter,
focusService: focusService,
autostarter: autostarter,
locator: locator,
@ -410,7 +426,7 @@ func (bridge *Bridge) Close(ctx context.Context) {
}, bridge.usersLock)
// Stop all ongoing tasks.
bridge.tasks.Wait()
bridge.tasks.CancelAndWait()
// Close the focus service.
bridge.focusService.Close()

View File

@ -484,6 +484,8 @@ func withBridge(
mocks.TLSReporter,
liteapi.NewDialer(netCtl, &tls.Config{InsecureSkipVerify: true}).GetRoundTripper(),
mocks.ProxyCtl,
mocks.CrashHandler,
mocks.Reporter,
// The logging stuff.
false,

View File

@ -29,12 +29,12 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v2/internal/async"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
"github.com/ProtonMail/proton-bridge/v2/internal/user"
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
"github.com/bradenaw/juniper/xsync"
"github.com/sirupsen/logrus"
)
@ -194,9 +194,10 @@ func newIMAPServer(
gluonDir string,
version *semver.Version,
tlsConfig *tls.Config,
reporter reporter.Reporter,
logClient, logServer bool,
eventCh chan<- imapEvents.Event,
tasks *xsync.Group,
tasks *async.Group,
) (*gluon.Server, error) {
logrus.WithFields(logrus.Fields{
"gluonDir": gluonDir,
@ -233,6 +234,7 @@ func newIMAPServer(
gluon.WithDataDir(gluonDir),
gluon.WithLogger(imapClientLog, imapServerLog),
getGluonVersionInfo(version),
gluon.WithReporter(reporter),
)
if err != nil {
return nil, err

View File

@ -21,6 +21,9 @@ type Mocks struct {
Updater *TestUpdater
Autostarter *mocks.MockAutostarter
CrashHandler *mocks.MockPanicHandler
Reporter *mocks.MockReporter
}
func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
@ -33,11 +36,17 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
Updater: NewTestUpdater(version, minAuto),
Autostarter: mocks.NewMockAutostarter(ctl),
CrashHandler: mocks.NewMockPanicHandler(ctl),
Reporter: mocks.NewMockReporter(ctl),
}
// When getting the TLS issue channel, we want to return the test channel.
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
// This is called at he end of any go-routine:
mocks.CrashHandler.EXPECT().HandlePanic().AnyTimes()
return mocks
}

View File

@ -0,0 +1,46 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/v2/internal/async (interfaces: PanicHandler)
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockPanicHandler is a mock of PanicHandler interface.
type MockPanicHandler struct {
ctrl *gomock.Controller
recorder *MockPanicHandlerMockRecorder
}
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler.
type MockPanicHandlerMockRecorder struct {
mock *MockPanicHandler
}
// NewMockPanicHandler creates a new mock instance.
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
mock := &MockPanicHandler{ctrl: ctrl}
mock.recorder = &MockPanicHandlerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
return m.recorder
}
// HandlePanic mocks base method.
func (m *MockPanicHandler) HandlePanic() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "HandlePanic")
}
// HandlePanic indicates an expected call of HandlePanic.
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
}

View File

@ -0,0 +1,90 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/gluon/reporter (interfaces: Reporter)
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockReporter is a mock of Reporter interface.
type MockReporter struct {
ctrl *gomock.Controller
recorder *MockReporterMockRecorder
}
// MockReporterMockRecorder is the mock recorder for MockReporter.
type MockReporterMockRecorder struct {
mock *MockReporter
}
// NewMockReporter creates a new mock instance.
func NewMockReporter(ctrl *gomock.Controller) *MockReporter {
mock := &MockReporter{ctrl: ctrl}
mock.recorder = &MockReporterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockReporter) EXPECT() *MockReporterMockRecorder {
return m.recorder
}
// ReportException mocks base method.
func (m *MockReporter) ReportException(arg0 interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportException", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ReportException indicates an expected call of ReportException.
func (mr *MockReporterMockRecorder) ReportException(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportException", reflect.TypeOf((*MockReporter)(nil).ReportException), arg0)
}
// ReportExceptionWithContext mocks base method.
func (m *MockReporter) ReportExceptionWithContext(arg0 interface{}, arg1 map[string]interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportExceptionWithContext", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// ReportExceptionWithContext indicates an expected call of ReportExceptionWithContext.
func (mr *MockReporterMockRecorder) ReportExceptionWithContext(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportExceptionWithContext", reflect.TypeOf((*MockReporter)(nil).ReportExceptionWithContext), arg0, arg1)
}
// ReportMessage mocks base method.
func (m *MockReporter) ReportMessage(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportMessage", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ReportMessage indicates an expected call of ReportMessage.
func (mr *MockReporterMockRecorder) ReportMessage(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessage", reflect.TypeOf((*MockReporter)(nil).ReportMessage), arg0)
}
// ReportMessageWithContext mocks base method.
func (m *MockReporter) ReportMessageWithContext(arg0 string, arg1 map[string]interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportMessageWithContext", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// ReportMessageWithContext indicates an expected call of ReportMessageWithContext.
func (mr *MockReporterMockRecorder) ReportMessageWithContext(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessageWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1)
}

View File

@ -0,0 +1,109 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package mocks
import (
"strings"
"gitlab.protontech.ch/go/liteapi"
)
type refreshContextMatcher struct {
wantRefresh liteapi.RefreshFlag
}
func NewRefreshContextMatcher(refreshFlag liteapi.RefreshFlag) *refreshContextMatcher { //nolint:revive
return &refreshContextMatcher{wantRefresh: refreshFlag}
}
func (m *refreshContextMatcher) Matches(x interface{}) bool {
context, ok := x.(map[string]interface{})
if !ok {
return false
}
i, ok := context["EventLoop"]
if !ok {
return false
}
el, ok := i.(map[string]interface{})
if !ok {
return false
}
vID, ok := el["EventID"]
if !ok {
return false
}
id, ok := vID.(string)
if !ok {
return false
}
if id == "" {
return false
}
vRefresh, ok := el["Refresh"]
if !ok {
return false
}
refresh, ok := vRefresh.(liteapi.RefreshFlag)
if !ok {
return false
}
return refresh == m.wantRefresh
}
func (m *refreshContextMatcher) String() string {
return `map[string]interface which contains "Refresh" field with value liteapi.RefreshAll`
}
type closedConnectionMatcher struct {
}
func NewClosedConnectionMatcher() *closedConnectionMatcher { //nolint:revive
return &closedConnectionMatcher{}
}
func (m *closedConnectionMatcher) Matches(x interface{}) bool {
context, ok := x.(map[string]interface{})
if !ok {
return false
}
vErr, ok := context["error"]
if !ok {
return false
}
err, ok := vErr.(error)
if !ok {
return false
}
return strings.Contains(err.Error(), "used of closed network connection")
}
func (m *closedConnectionMatcher) String() string {
return "map containing error of closed network connection"
}

View File

@ -32,6 +32,7 @@ import (
"github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"gitlab.protontech.ch/go/liteapi"
"gitlab.protontech.ch/go/liteapi/server"
@ -43,6 +44,8 @@ func TestBridge_Send(t *testing.T) {
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)

View File

@ -139,6 +139,7 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
bridge.vault.GetGluonDir(),
bridge.curVersion,
bridge.tlsConfig,
bridge.reporter,
bridge.logIMAPClient,
bridge.logIMAPServer,
bridge.imapEventCh,

View File

@ -32,6 +32,7 @@ import (
"github.com/bradenaw/juniper/iterator"
"github.com/bradenaw/juniper/stream"
"github.com/emersion/go-imap/client"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"gitlab.protontech.ch/go/liteapi"
"gitlab.protontech.ch/go/liteapi/server"
@ -69,6 +70,8 @@ func TestBridge_Sync(t *testing.T) {
// If we then connect an IMAP client, it should see all the messages.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
info, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.Connected)
@ -93,6 +96,8 @@ func TestBridge_Sync(t *testing.T) {
// Login the user; its sync should fail.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
{
syncCh, done := chToType[events.Event, events.SyncFailed](bridge.GetEvents(events.SyncFailed{}))
defer done()

View File

@ -101,6 +101,10 @@ func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) {
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte) (*liteapi.Client, liteapi.Auth, error) {
logrus.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
if username == "crash@bandicoot" {
panic("Your wish is my command.. I crash!")
}
client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password)
if err != nil {
return nil, liteapi.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
@ -415,6 +419,8 @@ func (bridge *Bridge) addUserWithVault(
vault,
client,
apiUser,
bridge.crashHandler,
bridge.reporter,
bridge.vault.SyncWorkers(),
bridge.vault.SyncBuffer(),
bridge.vault.GetShowAllMail(),

View File

@ -24,8 +24,10 @@ import (
"time"
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
mocksPkg "github.com/ProtonMail/proton-bridge/v2/internal/bridge/mocks"
"github.com/ProtonMail/proton-bridge/v2/internal/events"
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"gitlab.protontech.ch/go/liteapi"
"gitlab.protontech.ch/go/liteapi/server"
@ -612,6 +614,11 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
func TestBridge_User_Refresh(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *liteapi.NetCtl, locator bridge.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
mocks.Reporter.EXPECT().ReportMessageWithContext(
gomock.Eq("Warning: refresh occurred"),
mocksPkg.NewRefreshContextMatcher(liteapi.RefreshAll),
).Return(nil)
// Get a channel of sync started events.
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer done()