mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Launcher, app/base, sentry, update service
This commit is contained in:
@ -35,261 +35,50 @@ package main
|
||||
*/
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/api"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/cmd"
|
||||
"github.com/ProtonMail/proton-bridge/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/smtp"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/base"
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/bridge"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
// cacheVersion is used for cache files such as lock, events, preferences, user_info, db files.
|
||||
// Different number will drop old files and create new ones.
|
||||
cacheVersion = "c11"
|
||||
|
||||
appName = "bridge"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
|
||||
appName = "ProtonMail Bridge"
|
||||
appUsage = "ProtonMail IMAP and SMTP Bridge"
|
||||
configName = "bridge"
|
||||
updateURLName = "bridge"
|
||||
keychainName = "bridge"
|
||||
cacheVersion = "c11"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Main(
|
||||
"ProtonMail Bridge",
|
||||
"ProtonMail IMAP and SMTP Bridge",
|
||||
[]cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "no-window",
|
||||
Usage: "Don't show window after start"},
|
||||
cli.BoolFlag{
|
||||
Name: "noninteractive",
|
||||
Usage: "Start Bridge entirely noninteractively"},
|
||||
},
|
||||
run,
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
if err := base.MigrateFiles(configName); err != nil {
|
||||
logrus.WithError(err).Warn("Old config files could not be migrated")
|
||||
}
|
||||
|
||||
os.Args = base.StripProcessSerialNumber(os.Args)
|
||||
|
||||
base, err := base.New(
|
||||
appName,
|
||||
appUsage,
|
||||
configName,
|
||||
updateURLName,
|
||||
keychainName,
|
||||
cacheVersion,
|
||||
)
|
||||
}
|
||||
|
||||
// run initializes and starts everything in a precise order.
|
||||
//
|
||||
// IMPORTANT: ***Read the comments before CHANGING the order ***
|
||||
func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
||||
// We need to have config instance to setup a logs, panic handler, etc ...
|
||||
cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion)
|
||||
|
||||
// We want to know about any problem. Our PanicHandler calls sentry which is
|
||||
// not dependent on anything else. If that fails, it tries to create crash
|
||||
// report which will not be possible if no folder can be created. That's the
|
||||
// only problem we will not be notified about in any way.
|
||||
panicHandler := &cmd.PanicHandler{
|
||||
AppName: "ProtonMail Bridge",
|
||||
Config: cfg,
|
||||
Err: &contextError,
|
||||
}
|
||||
defer panicHandler.HandlePanic()
|
||||
|
||||
// First we need config and create necessary folder; it's dependency for everything.
|
||||
if err := cfg.CreateDirs(); err != nil {
|
||||
log.Fatal("Cannot create necessary folders: ", err)
|
||||
}
|
||||
|
||||
// Setup of logs should be as soon as possible to ensure we record every wanted report in the log.
|
||||
logLevel := context.GlobalString("log-level")
|
||||
debugClient, debugServer := config.SetupLog(cfg, logLevel)
|
||||
|
||||
// Doesn't make sense to continue when Bridge was invoked with wrong arguments.
|
||||
// We should tell that to the user before we do anything else.
|
||||
if context.Args().First() != "" {
|
||||
_ = cli.ShowAppHelp(context)
|
||||
return cli.NewExitError("Unknown argument", 4)
|
||||
}
|
||||
|
||||
// It's safe to get version JSON file even when other instance is running.
|
||||
// (thus we put it before check of presence of other Bridge instance).
|
||||
updates := updates.NewBridge(cfg.GetUpdateDir())
|
||||
|
||||
if dir := context.GlobalString("version-json"); dir != "" {
|
||||
cmd.GenerateVersionFiles(updates, dir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Should be called after logs are configured but before preferences are created.
|
||||
migratePreferencesFromC10(cfg)
|
||||
|
||||
// ClearOldData before starting new bridge to do a proper setup.
|
||||
//
|
||||
// IMPORTANT: If you the change position of this you will need to wait
|
||||
// until force-update to be applied on all currently used bridge
|
||||
// versions
|
||||
if err := cfg.ClearOldData(); err != nil {
|
||||
log.Error("Cannot clear old data: ", err)
|
||||
}
|
||||
|
||||
// GetTLSConfig is needed for IMAP, SMTL and local bridge API (to check second instance).
|
||||
//
|
||||
// This should be called after ClearOldData, in order to re-create the
|
||||
// certificates if clean data will remove them (accidentally or on purpose).
|
||||
tls, err := config.GetTLSConfig(cfg)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Cannot get TLS certificate")
|
||||
}
|
||||
|
||||
pref := preferences.New(cfg)
|
||||
|
||||
// Now we can try to proceed with starting the bridge. First we need to ensure
|
||||
// this is the only instance. If not, we will end and focus the existing one.
|
||||
lock, err := singleinstance.CreateLockFile(cfg.GetLockPath())
|
||||
if err != nil {
|
||||
log.Warn("Bridge is already running")
|
||||
if err := api.CheckOtherInstanceAndFocus(pref.GetInt(preferences.APIPortKey), tls); err != nil {
|
||||
cmd.DisableRestart()
|
||||
log.Error("Second instance: ", err)
|
||||
}
|
||||
return cli.NewExitError("Bridge is already running.", 3)
|
||||
}
|
||||
defer lock.Close() //nolint[errcheck]
|
||||
|
||||
// In case user wants to do CPU or memory profiles...
|
||||
if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile {
|
||||
cmd.StartCPUProfile()
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
|
||||
defer cmd.MakeMemoryProfile()
|
||||
}
|
||||
|
||||
// Now we initialize all Bridge parts.
|
||||
log.Debug("Initializing bridge...")
|
||||
eventListener := listener.New()
|
||||
events.SetupEvents(eventListener)
|
||||
|
||||
credentialsStore, credentialsError := credentials.NewStore(appName)
|
||||
if credentialsError != nil {
|
||||
log.Error("Could not get credentials store: ", credentialsError)
|
||||
}
|
||||
|
||||
cm := pmapi.NewClientManager(cfg.GetAPIConfig())
|
||||
|
||||
// Different build types have different roundtrippers (e.g. we want to enable
|
||||
// TLS fingerprint checks in production builds). GetRoundTripper has a different
|
||||
// implementation depending on whether build flag pmapi_prod is used or not.
|
||||
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
|
||||
|
||||
// Cookies must be persisted across restarts.
|
||||
jar, err := cookies.NewCookieJar(pref)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Warn("Could not create cookie jar")
|
||||
} else {
|
||||
cm.SetCookieJar(jar)
|
||||
}
|
||||
|
||||
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, cm, credentialsStore)
|
||||
imapBackend := imap.NewIMAPBackend(panicHandler, eventListener, cfg, bridgeInstance)
|
||||
smtpBackend := smtp.NewSMTPBackend(panicHandler, eventListener, pref, bridgeInstance)
|
||||
|
||||
go func() {
|
||||
defer panicHandler.HandlePanic()
|
||||
apiServer := api.NewAPIServer(pref, tls, cfg.GetTLSCertPath(), cfg.GetTLSKeyPath(), eventListener)
|
||||
apiServer.ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer panicHandler.HandlePanic()
|
||||
imapPort := pref.GetInt(preferences.IMAPPortKey)
|
||||
imapServer := imap.NewIMAPServer(debugClient, debugServer, imapPort, tls, imapBackend, eventListener)
|
||||
imapServer.ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer panicHandler.HandlePanic()
|
||||
smtpPort := pref.GetInt(preferences.SMTPPortKey)
|
||||
useSSL := pref.GetBool(preferences.SMTPSSLKey)
|
||||
smtpServer := smtp.NewSMTPServer(debugClient || debugServer, smtpPort, useSSL, tls, smtpBackend, eventListener)
|
||||
smtpServer.ListenAndServe()
|
||||
}()
|
||||
|
||||
// Decide about frontend mode before initializing rest of bridge.
|
||||
var frontendMode string
|
||||
|
||||
switch {
|
||||
case context.GlobalBool("cli"):
|
||||
frontendMode = "cli"
|
||||
case context.GlobalBool("noninteractive"):
|
||||
frontendMode = "noninteractive"
|
||||
default:
|
||||
frontendMode = "qt"
|
||||
}
|
||||
|
||||
log.WithField("mode", frontendMode).Debug("Determined frontend mode to use")
|
||||
|
||||
// If we are starting bridge in noninteractive mode, simply block instead of starting a frontend.
|
||||
if frontendMode == "noninteractive" {
|
||||
<-(make(chan struct{}))
|
||||
return nil
|
||||
}
|
||||
|
||||
showWindowOnStart := !context.GlobalBool("no-window")
|
||||
frontend := frontend.New(constants.Version, constants.BuildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend)
|
||||
|
||||
// Last part is to start everything.
|
||||
log.Debug("Starting frontend...")
|
||||
if err := frontend.Loop(credentialsError); err != nil {
|
||||
log.Error("Frontend failed with error: ", err)
|
||||
return cli.NewExitError("Frontend error", 2)
|
||||
}
|
||||
|
||||
if frontend.IsAppRestarting() {
|
||||
cmd.RestartApp()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migratePreferencesFromC10 will copy preferences from c10 folder to c11.
|
||||
// It will happen only when c10/prefs.json exists and c11/prefs.json not.
|
||||
// No configuration changed between c10 and c11 versions.
|
||||
func migratePreferencesFromC10(cfg *config.Config) {
|
||||
pref10Path := config.New(appName, constants.Version, constants.Revision, "c10").GetPreferencesPath()
|
||||
if _, err := os.Stat(pref10Path); os.IsNotExist(err) {
|
||||
log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped")
|
||||
return
|
||||
}
|
||||
|
||||
pref11Path := cfg.GetPreferencesPath()
|
||||
if _, err := os.Stat(pref11Path); err == nil {
|
||||
log.WithField("path", pref11Path).Trace("New preferences already exists, migration skipped")
|
||||
return
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(pref10Path) //nolint[gosec]
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Problem to load old preferences")
|
||||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(pref11Path, data, 0600)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Problem to migrate preferences")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Preferences migrated")
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to create app base")
|
||||
}
|
||||
// Other instance already running.
|
||||
if base == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := bridge.New(base).Run(os.Args); err != nil {
|
||||
logrus.WithError(err).Fatal("Bridge exited with error")
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,160 +18,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"runtime/pprof"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/cmd"
|
||||
"github.com/ProtonMail/proton-bridge/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/base"
|
||||
"github.com/ProtonMail/proton-bridge/internal/app/ie"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
// cacheVersion is used for cache files such as lock, or preferences.
|
||||
// Different number will drop old files and create new ones.
|
||||
cacheVersion = "c11"
|
||||
|
||||
appName = "importExport"
|
||||
appNameDash = "import-export-app"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
|
||||
appName = "ProtonMail Import-Export app"
|
||||
appUsage = "Import and export messages to/from your ProtonMail account"
|
||||
configName = "importExport"
|
||||
updateURLName = "ie"
|
||||
keychainName = "import-export-app"
|
||||
cacheVersion = "c11"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Main(
|
||||
"ProtonMail Import-Export",
|
||||
"ProtonMail Import-Export app",
|
||||
nil,
|
||||
run,
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
if err := base.MigrateFiles(configName); err != nil {
|
||||
logrus.WithError(err).Warn("Old config files could not be migrated")
|
||||
}
|
||||
|
||||
os.Args = base.StripProcessSerialNumber(os.Args)
|
||||
|
||||
base, err := base.New(
|
||||
appName,
|
||||
appUsage,
|
||||
configName,
|
||||
updateURLName,
|
||||
keychainName,
|
||||
cacheVersion,
|
||||
)
|
||||
}
|
||||
|
||||
// run initializes and starts everything in a precise order.
|
||||
//
|
||||
// IMPORTANT: ***Read the comments before CHANGING the order ***
|
||||
func run(context *cli.Context) (contextError error) { // nolint[funlen]
|
||||
// We need to have config instance to setup a logs, panic handler, etc ...
|
||||
cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion)
|
||||
|
||||
// We want to know about any problem. Our PanicHandler calls sentry which is
|
||||
// not dependent on anything else. If that fails, it tries to create crash
|
||||
// report which will not be possible if no folder can be created. That's the
|
||||
// only problem we will not be notified about in any way.
|
||||
panicHandler := &cmd.PanicHandler{
|
||||
AppName: "ProtonMail Import-Export app",
|
||||
Config: cfg,
|
||||
Err: &contextError,
|
||||
}
|
||||
defer panicHandler.HandlePanic()
|
||||
|
||||
// First we need config and create necessary folder; it's dependency for everything.
|
||||
if err := cfg.CreateDirs(); err != nil {
|
||||
log.Fatal("Cannot create necessary folders: ", err)
|
||||
}
|
||||
|
||||
// Setup of logs should be as soon as possible to ensure we record every wanted report in the log.
|
||||
logLevel := context.GlobalString("log-level")
|
||||
_, _ = config.SetupLog(cfg, logLevel)
|
||||
|
||||
// Doesn't make sense to continue when Import-Export was invoked with wrong arguments.
|
||||
// We should tell that to the user before we do anything else.
|
||||
if context.Args().First() != "" {
|
||||
_ = cli.ShowAppHelp(context)
|
||||
return cli.NewExitError("Unknown argument", 4)
|
||||
}
|
||||
|
||||
// It's safe to get version JSON file even when other instance is running.
|
||||
// (thus we put it before check of presence of other Import-Export instance).
|
||||
updates := updates.NewImportExport(cfg.GetUpdateDir())
|
||||
|
||||
if dir := context.GlobalString("version-json"); dir != "" {
|
||||
cmd.GenerateVersionFiles(updates, dir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Now we can try to proceed with starting the Import-Export. First we need to ensure
|
||||
// this is the only instance. If not, we will end and focus the existing one.
|
||||
lock, err := singleinstance.CreateLockFile(cfg.GetLockPath())
|
||||
if err != nil {
|
||||
log.Warn("Import-Export app is already running")
|
||||
return cli.NewExitError("Import-Export app is already running.", 3)
|
||||
logrus.WithError(err).Fatal("Failed to create app base")
|
||||
}
|
||||
defer lock.Close() //nolint[errcheck]
|
||||
|
||||
// In case user wants to do CPU or memory profiles...
|
||||
if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile {
|
||||
cmd.StartCPUProfile()
|
||||
defer pprof.StopCPUProfile()
|
||||
// Other instance already running.
|
||||
if base == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
|
||||
defer cmd.MakeMemoryProfile()
|
||||
if err := ie.New(base).Run(os.Args); err != nil {
|
||||
logrus.WithError(err).Fatal("IE exited with error")
|
||||
}
|
||||
|
||||
// Now we initialize all Import-Export parts.
|
||||
log.Debug("Initializing import-export...")
|
||||
eventListener := listener.New()
|
||||
events.SetupEvents(eventListener)
|
||||
|
||||
credentialsStore, credentialsError := credentials.NewStore(appNameDash)
|
||||
if credentialsError != nil {
|
||||
log.Error("Could not get credentials store: ", credentialsError)
|
||||
}
|
||||
|
||||
cm := pmapi.NewClientManager(cfg.GetAPIConfig())
|
||||
|
||||
// Different build types have different roundtrippers (e.g. we want to enable
|
||||
// TLS fingerprint checks in production builds). GetRoundTripper has a different
|
||||
// implementation depending on whether build flag pmapi_prod is used or not.
|
||||
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
|
||||
|
||||
pref := preferences.New(cfg)
|
||||
|
||||
// Cookies must be persisted across restarts.
|
||||
jar, err := cookies.NewCookieJar(pref)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Warn("Could not create cookie jar")
|
||||
} else {
|
||||
cm.SetCookieJar(jar)
|
||||
}
|
||||
|
||||
importexportInstance := importexport.New(cfg, panicHandler, eventListener, cm, credentialsStore)
|
||||
|
||||
// Decide about frontend mode before initializing rest of import-export.
|
||||
var frontendMode string
|
||||
switch {
|
||||
case context.GlobalBool("cli"):
|
||||
frontendMode = "cli"
|
||||
default:
|
||||
frontendMode = "qt"
|
||||
}
|
||||
log.WithField("mode", frontendMode).Debug("Determined frontend mode to use")
|
||||
|
||||
frontend := frontend.NewImportExport(constants.Version, constants.BuildVersion, frontendMode, panicHandler, cfg, eventListener, updates, importexportInstance)
|
||||
|
||||
// Last part is to start everything.
|
||||
log.Debug("Starting frontend...")
|
||||
if err := frontend.Loop(credentialsError); err != nil {
|
||||
log.Error("Frontend failed with error: ", err)
|
||||
return cli.NewExitError("Frontend error", 2)
|
||||
}
|
||||
|
||||
if frontend.IsAppRestarting() {
|
||||
cmd.RestartApp()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
177
cmd/launcher/main.go
Normal file
177
cmd/launcher/main.go
Normal file
@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/go-appdir"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/internal/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const appName = "ProtonMail Launcher"
|
||||
|
||||
var (
|
||||
ConfigName = "" // nolint[gochecknoglobals]
|
||||
ExeName = "" // nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
func main() { // nolint[funlen]
|
||||
sentryReporter := sentry.NewReporter(appName, constants.Version)
|
||||
|
||||
crashHandler := crash.NewHandler(sentryReporter.Report)
|
||||
defer crashHandler.HandlePanic()
|
||||
|
||||
locations := locations.New(
|
||||
appdir.New(filepath.Join(constants.VendorName, ConfigName)),
|
||||
ConfigName,
|
||||
)
|
||||
|
||||
logsPath, err := locations.ProvideLogsPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to get logs path")
|
||||
}
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
if err := logging.Init(logsPath); err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to setup logging")
|
||||
}
|
||||
|
||||
logging.SetLevel(os.Getenv("VERBOSITY"))
|
||||
|
||||
updatesPath, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to get updates path")
|
||||
}
|
||||
|
||||
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to create new verification key")
|
||||
}
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to create new verification keyring")
|
||||
}
|
||||
|
||||
versioner := versioner.New(updatesPath)
|
||||
|
||||
exe, err := getPathToExecutable(ExeName, versioner, kr)
|
||||
if err != nil {
|
||||
if exe, err = getFallbackExecutable(ExeName, versioner); err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to find any launchable executable")
|
||||
}
|
||||
}
|
||||
|
||||
launcher, err := os.Executable()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to determine path to launcher")
|
||||
}
|
||||
|
||||
cmd := exec.Command(exe, appendLauncherPath(launcher, os.Args[1:])...) // nolint[gosec]
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// On windows, if you use Run(), a terminal stays open; we don't want that.
|
||||
if runtime.GOOS == "windows" {
|
||||
err = cmd.Start()
|
||||
} else {
|
||||
err = cmd.Run()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to launch")
|
||||
}
|
||||
}
|
||||
|
||||
func appendLauncherPath(path string, args []string) []string {
|
||||
res := append([]string{}, args...)
|
||||
|
||||
hasFlag := false
|
||||
|
||||
for k, v := range res {
|
||||
if v != "--launcher" {
|
||||
continue
|
||||
}
|
||||
|
||||
hasFlag = true
|
||||
|
||||
if k+1 >= len(res) {
|
||||
continue
|
||||
}
|
||||
|
||||
res[k+1] = path
|
||||
}
|
||||
|
||||
if !hasFlag {
|
||||
res = append(res, "--launcher", path)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func getPathToExecutable(name string, versioner *versioner.Versioner, kr *crypto.KeyRing) (string, error) {
|
||||
versions, err := versioner.ListVersions()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to list available versions")
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
vlog := logrus.WithField("version", version)
|
||||
|
||||
if err := version.VerifyFiles(kr); err != nil {
|
||||
vlog.WithError(err).Error("Failed to verify files")
|
||||
continue
|
||||
}
|
||||
|
||||
exe, err := version.GetExecutable(name)
|
||||
if err != nil {
|
||||
vlog.WithError(err).Error("Failed to get executable")
|
||||
continue
|
||||
}
|
||||
|
||||
return exe, nil
|
||||
}
|
||||
|
||||
return "", errors.New("no available versions")
|
||||
}
|
||||
|
||||
func getFallbackExecutable(name string, versioner *versioner.Versioner) (string, error) {
|
||||
logrus.Info("Searching for fallback executable")
|
||||
|
||||
launcher, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to determine path to launcher")
|
||||
}
|
||||
|
||||
return versioner.GetExecutableInDirectory(name, filepath.Dir(launcher))
|
||||
}
|
||||
179
cmd/versioner/main.go
Normal file
179
cmd/versioner/main.go
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type versionInfo struct {
|
||||
updater.VersionInfo
|
||||
|
||||
Commit string
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := createApp().Run(os.Args); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func createApp() *cli.App { // nolint[funlen]
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = "versioner"
|
||||
|
||||
app.Usage = "Create and update version files"
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "app",
|
||||
Usage: "The app (bridge, importExport)",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "platform",
|
||||
Usage: "The platform (windows, darwin, linux)",
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
|
||||
app.Commands = []*cli.Command{{
|
||||
Name: "update",
|
||||
Action: update,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "channel",
|
||||
Usage: "The update channel (live/beta/...)",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "version",
|
||||
Usage: "The version of the app",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "min-auto",
|
||||
Usage: "The minimum version of the app that can autoupdate to this version",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "package",
|
||||
Usage: "The package file",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "installer",
|
||||
Usage: "An installer that can be used to manually install the app (can be specified multiple times)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "landing",
|
||||
Usage: "The landing page",
|
||||
},
|
||||
&cli.Float64Flag{
|
||||
Name: "rollout",
|
||||
Usage: "What proportion of users should receive this update",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "commit",
|
||||
Usage: "What commit produced this update",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "dump",
|
||||
Action: dump,
|
||||
}}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func update(c *cli.Context) error {
|
||||
versions := fetch(c.String("app"), c.String("platform"))
|
||||
|
||||
version := versions[c.String("channel")]
|
||||
|
||||
if c.IsSet("version") {
|
||||
version.Version = semver.MustParse(c.String("version"))
|
||||
}
|
||||
|
||||
if c.IsSet("min-auto") {
|
||||
version.MinAuto = semver.MustParse(c.String("min-auto"))
|
||||
}
|
||||
|
||||
if c.IsSet("package") {
|
||||
version.Package = c.String("package")
|
||||
}
|
||||
|
||||
if c.IsSet("installer") {
|
||||
version.Installers = c.StringSlice("installer")
|
||||
}
|
||||
|
||||
if c.IsSet("landing") {
|
||||
version.Landing = c.String("landing")
|
||||
}
|
||||
|
||||
if c.IsSet("rollout") {
|
||||
version.Rollout = c.Float64("rollout")
|
||||
}
|
||||
|
||||
if c.IsSet("commit") {
|
||||
version.Commit = c.String("commit")
|
||||
}
|
||||
|
||||
versions[c.String("channel")] = version
|
||||
|
||||
return write(c.App.Writer, versions)
|
||||
}
|
||||
|
||||
func dump(c *cli.Context) error {
|
||||
return write(c.App.Writer, fetch(c.String("app"), c.String("platform")))
|
||||
}
|
||||
|
||||
func fetch(app, platform string) map[string]versionInfo {
|
||||
url := fmt.Sprintf(
|
||||
"%v/%v/version_%v.json",
|
||||
updater.Host, app, platform,
|
||||
)
|
||||
|
||||
res, err := resty.New().R().Get(url)
|
||||
if err != nil {
|
||||
return make(map[string]versionInfo)
|
||||
}
|
||||
|
||||
var versionMap map[string]versionInfo
|
||||
|
||||
if err := json.Unmarshal(res.Body(), &versionMap); err != nil {
|
||||
return make(map[string]versionInfo)
|
||||
}
|
||||
|
||||
return versionMap
|
||||
}
|
||||
|
||||
func write(w io.Writer, versions map[string]versionInfo) error {
|
||||
enc := json.NewEncoder(w)
|
||||
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
return enc.Encode(versions)
|
||||
}
|
||||
Reference in New Issue
Block a user