diff --git a/internal/app/base/base.go b/internal/app/base/base.go index 355ff05c..059197c6 100644 --- a/internal/app/base/base.go +++ b/internal/app/base/base.go @@ -73,7 +73,8 @@ const ( FlagCLI = "cli" flagCLIShort = "c" flagRestart = "restart" - flagLauncher = "launcher" + FlagLauncher = "launcher" + FlagNoWindow = "no-window" ) type Base struct { @@ -235,7 +236,7 @@ func New( // nolint[funlen] autostart := &autostart.App{ Name: appName, DisplayName: appName, - Exec: []string{exe}, + Exec: []string{exe, "--" + FlagNoWindow}, } return &Base{ @@ -264,13 +265,13 @@ func New( // nolint[funlen] }, nil } -func (b *Base) NewApp(action func(*Base, *cli.Context) error) *cli.App { +func (b *Base) NewApp(mainLoop func(*Base, *cli.Context) error) *cli.App { app := cli.NewApp() app.Name = b.Name app.Usage = b.usage app.Version = constants.Version - app.Action = b.run(action) + app.Action = b.wrapMainLoop(mainLoop) app.Flags = []cli.Flag{ &cli.BoolFlag{ Name: flagCPUProfile, @@ -292,13 +293,17 @@ func (b *Base) NewApp(action func(*Base, *cli.Context) error) *cli.App { Aliases: []string{flagCLIShort}, Usage: "Use command line interface", }, + &cli.BoolFlag{ + Name: FlagNoWindow, + Usage: "Don't show window after start", + }, &cli.StringFlag{ Name: flagRestart, Usage: "The number of times the application has already restarted", Hidden: true, }, &cli.StringFlag{ - Name: flagLauncher, + Name: FlagLauncher, Usage: "The launcher to use to restart the application", Hidden: true, }, @@ -317,15 +322,18 @@ func (b *Base) AddTeardownAction(fn func() error) { b.teardown = append(b.teardown, fn) } -func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc { // nolint[funlen] +func (b *Base) wrapMainLoop(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc { // nolint[funlen] return func(c *cli.Context) error { defer b.CrashHandler.HandlePanic() defer func() { _ = b.Lock.Close() }() - // If launcher was used to start the app, use that for restart/autostart. - if launcher := c.String(flagLauncher); launcher != "" { - b.Autostart.Exec = []string{launcher} + // If launcher was used to start the app, use that for restart + // and autostart. + if launcher := c.String(FlagLauncher); launcher != "" { b.command = launcher + // Bridge supports no-window option which we should use + // for autostart. + b.Autostart.Exec = []string{launcher, "--" + FlagNoWindow} } if c.Bool(flagCPUProfile) { diff --git a/internal/app/bridge/bridge.go b/internal/app/bridge/bridge.go index 47b47f5b..ea304e69 100644 --- a/internal/app/bridge/bridge.go +++ b/internal/app/bridge/bridge.go @@ -44,7 +44,6 @@ import ( const ( flagLogIMAP = "log-imap" flagLogSMTP = "log-smtp" - flagNoWindow = "no-window" flagNonInteractive = "noninteractive" // Memory cache was estimated by empirical usage in past and it was set to 100MB. @@ -53,7 +52,7 @@ const ( ) func New(base *base.Base) *cli.App { - app := base.NewApp(run) + app := base.NewApp(mailLoop) app.Flags = append(app.Flags, []cli.Flag{ &cli.StringFlag{ @@ -62,9 +61,6 @@ func New(base *base.Base) *cli.App { &cli.BoolFlag{ Name: flagLogSMTP, Usage: "Enable logging of SMTP communications (may contain decrypted data!)"}, - &cli.BoolFlag{ - Name: flagNoWindow, - Usage: "Don't show window after start"}, &cli.BoolFlag{ Name: flagNonInteractive, Usage: "Start Bridge entirely noninteractively"}, @@ -73,7 +69,7 @@ func New(base *base.Base) *cli.App { return app } -func run(b *base.Base, c *cli.Context) error { // nolint[funlen] +func mailLoop(b *base.Base, c *cli.Context) error { // nolint[funlen] tlsConfig, err := loadTLSConfig(b) if err != nil { return err @@ -89,7 +85,21 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen] b.Settings.GetInt(settings.AttachmentWorkers), ) - bridge := pkgBridge.New(b.Locations, b.Cache, b.Settings, b.SentryReporter, b.CrashHandler, b.Listener, cache, builder, b.CM, b.Creds, b.Updater, b.Versioner) + bridge := pkgBridge.New( + b.Locations, + b.Cache, + b.Settings, + b.SentryReporter, + b.CrashHandler, + b.Listener, + cache, + builder, + b.CM, + b.Creds, + b.Updater, + b.Versioner, + b.Autostart, + ) imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, b.Settings, bridge) smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge) @@ -122,9 +132,6 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen] smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe() }() - // Bridge supports no-window option which we should use for autostart. - b.Autostart.Exec = append(b.Autostart.Exec, "--"+flagNoWindow) - // We want to remove old versions if the app exits successfully. b.AddTeardownAction(b.Versioner.RemoveOldVersions) @@ -147,7 +154,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen] constants.BuildVersion, b.Name, frontendMode, - !c.Bool(flagNoWindow), + !c.Bool(base.FlagNoWindow), b.CrashHandler, b.Locations, b.Settings, @@ -156,7 +163,6 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen] b.UserAgent, bridge, smtpBackend, - b.Autostart, b, ) diff --git a/internal/bridge/autostart.go b/internal/bridge/autostart.go new file mode 100644 index 00000000..4b028eb7 --- /dev/null +++ b/internal/bridge/autostart.go @@ -0,0 +1,31 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail 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. +// +// ProtonMail 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 ProtonMail Bridge. If not, see . + +// Package bridge provides core functionality of Bridge app. +package bridge + +func (b *Bridge) IsAutostartEnabled() bool { + return b.autostart.IsEnabled() +} + +func (b *Bridge) EnableAutostart() error { + return b.autostart.Enable() +} + +func (b *Bridge) DisableAutostart() error { + return b.autostart.Disable() +} diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index 81d3fe08..4ad90754 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -25,6 +25,7 @@ import ( "time" "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/go-autostart" "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/metrics" @@ -52,8 +53,11 @@ type Bridge struct { updater Updater versioner Versioner cacheProvider CacheProvider + autostart *autostart.App // Bridge's global errors list. errors []error + + lastVersion string } func New( @@ -69,6 +73,7 @@ func New( credStorer users.CredentialsStorer, updater Updater, versioner Versioner, + autostart *autostart.App, ) *Bridge { // Allow DoH before starting the app if the user has previously set this setting. // This allows us to start even if protonmail is blocked. @@ -94,6 +99,7 @@ func New( updater: updater, versioner: versioner, cacheProvider: cacheProvider, + autostart: autostart, } if setting.GetBool(settings.FirstStartKey) { @@ -101,9 +107,17 @@ func New( logrus.WithError(err).Error("Failed to send metric") } + if err := b.EnableAutostart(); err != nil { + log.WithError(err).Error("Failed to enable autostart") + } + setting.SetBool(settings.FirstStartKey, false) } + // Keep in bridge and update in settings the last used version. + b.lastVersion = b.settings.Get(settings.LastVersionKey) + b.settings.Set(settings.LastVersionKey, constants.Version) + go b.heartbeat() return b @@ -279,3 +293,8 @@ func (b *Bridge) HasError(err error) bool { return false } + +// GetLastVersion returns the version which was used in previous execution of Bridge. +func (b *Bridge) GetLastVersion() string { + return b.lastVersion +} diff --git a/internal/frontend/frontend.go b/internal/frontend/frontend.go index cf15eedf..0685e525 100644 --- a/internal/frontend/frontend.go +++ b/internal/frontend/frontend.go @@ -19,7 +19,6 @@ package frontend import ( - "github.com/ProtonMail/go-autostart" "github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/useragent" @@ -55,7 +54,6 @@ func New( userAgent *useragent.UserAgent, bridge *bridge.Bridge, noEncConfirmator types.NoEncConfirmator, - autostart *autostart.App, restarter types.Restarter, ) Frontend { bridgeWrap := types.NewBridgeWrap(bridge) @@ -74,7 +72,6 @@ func New( userAgent, bridgeWrap, noEncConfirmator, - autostart, restarter, ) case "cli": diff --git a/internal/frontend/qt/frontend.go b/internal/frontend/qt/frontend.go index 1497582e..64572821 100644 --- a/internal/frontend/qt/frontend.go +++ b/internal/frontend/qt/frontend.go @@ -25,7 +25,6 @@ import ( "fmt" "sync" - "github.com/ProtonMail/go-autostart" "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/useragent" "github.com/ProtonMail/proton-bridge/internal/frontend/types" @@ -50,7 +49,6 @@ type FrontendQt struct { userAgent *useragent.UserAgent bridge types.Bridger noEncConfirmator types.NoEncConfirmator - autostart *autostart.App restarter types.Restarter showOnStartup bool @@ -82,7 +80,6 @@ func New( userAgent *useragent.UserAgent, bridge types.Bridger, _ types.NoEncConfirmator, - autostart *autostart.App, restarter types.Restarter, ) *FrontendQt { userAgent.SetPlatform(core.QSysInfo_PrettyProductName()) @@ -99,7 +96,6 @@ func New( updater: updater, userAgent: userAgent, bridge: bridge, - autostart: autostart, restarter: restarter, showOnStartup: showWindowOnStart, } @@ -122,6 +118,12 @@ func (f *FrontendQt) Loop() error { f.watchEvents() }() + // Set whether this is the first time GUI starts. + f.qml.SetIsFirstGUIStart(f.settings.GetBool(settings.FirstStartGUIKey)) + defer func() { + f.settings.SetBool(settings.FirstStartGUIKey, false) + }() + if ret := f.app.Exec(); ret != 0 { err := fmt.Errorf("Event loop ended with return value: %v", ret) f.log.Warn("App exec", err) diff --git a/internal/frontend/qt/frontend_nogui.go b/internal/frontend/qt/frontend_nogui.go index dfe96f03..0381e0f6 100644 --- a/internal/frontend/qt/frontend_nogui.go +++ b/internal/frontend/qt/frontend_nogui.go @@ -23,7 +23,6 @@ import ( "fmt" "net/http" - "github.com/ProtonMail/go-autostart" "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/useragent" "github.com/ProtonMail/proton-bridge/internal/frontend/types" @@ -50,7 +49,6 @@ func New( userAgent *useragent.UserAgent, bridge types.Bridger, noEncConfirmator types.NoEncConfirmator, - autostart *autostart.App, restarter types.Restarter, ) *FrontendHeadless { return &FrontendHeadless{} diff --git a/internal/frontend/qt/frontend_settings.go b/internal/frontend/qt/frontend_settings.go index 0e00102b..ab56970b 100644 --- a/internal/frontend/qt/frontend_settings.go +++ b/internal/frontend/qt/frontend_settings.go @@ -75,21 +75,21 @@ func (f *FrontendQt) changeLocalCache(enableDiskCache bool, diskCachePath *core. } func (f *FrontendQt) setIsAutostartOn() { - f.qml.SetIsAutostartOn(f.autostart.IsEnabled()) + f.qml.SetIsAutostartOn(f.bridge.IsAutostartEnabled()) } func (f *FrontendQt) toggleAutostart(makeItEnabled bool) { defer f.qml.ToggleAutostartFinished() - if makeItEnabled == f.autostart.IsEnabled() { + if makeItEnabled == f.bridge.IsAutostartEnabled() { f.setIsAutostartOn() return } var err error if makeItEnabled { - err = f.autostart.Enable() + err = f.bridge.EnableAutostart() } else { - err = f.autostart.Disable() + err = f.bridge.DisableAutostart() } f.setIsAutostartOn() diff --git a/internal/frontend/qt/qml_backend.go b/internal/frontend/qt/qml_backend.go index 9bd44e89..72bc56e0 100644 --- a/internal/frontend/qt/qml_backend.go +++ b/internal/frontend/qt/qml_backend.go @@ -90,6 +90,8 @@ type QMLBackend struct { _ func(enableDiskCache bool, diskCachePath core.QUrl) `slot:"changeLocalCache"` _ func() `signal:"changeLocalCacheFinished"` + _ bool `property:"isFirstGUIStart"` + _ bool `property:"isAutomaticUpdateOn"` _ func(makeItActive bool) `slot:"toggleAutomaticUpdate"` diff --git a/internal/frontend/types/types.go b/internal/frontend/types/types.go index 8a5690d7..fa053bc5 100644 --- a/internal/frontend/types/types.go +++ b/internal/frontend/types/types.go @@ -87,6 +87,9 @@ type Bridger interface { GetKeychainApp() string SetKeychainApp(keychain string) HasError(err error) bool + IsAutostartEnabled() bool + EnableAutostart() error + DisableAutostart() error } type bridgeWrap struct { diff --git a/test/context/bridge.go b/test/context/bridge.go index c48bbc5f..d64bb26a 100644 --- a/test/context/bridge.go +++ b/test/context/bridge.go @@ -20,6 +20,7 @@ package context import ( "time" + "github.com/ProtonMail/go-autostart" "github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/useragent" @@ -87,5 +88,9 @@ func newBridgeInstance( credStore, newFakeUpdater(), newFakeVersioner(), + &autostart.App{ + Name: "bridge", + Exec: []string{"bridge"}, + }, ) }