forked from Silverfish/proton-bridge
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| debe87f2f5 | |||
| cca2807256 | |||
| 7b73f76e78 | |||
| b1eefd6c85 | |||
| bbcb7ad980 | |||
| 984c43cd75 | |||
| ec4c0fdd09 | |||
| 51d4a9c7ee | |||
| 19930f63e2 | |||
| 3b9a3aaad2 | |||
| f5148074fd | |||
| a949a113cf | |||
| 227e9df419 | |||
| 2a6d462be1 | |||
| bb03fa26cd |
26
Changelog.md
26
Changelog.md
@ -2,6 +2,32 @@
|
|||||||
|
|
||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
## [Bridge 2.4.8] Osney
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2071: Fix --no-window flag that was broken on Windows.
|
||||||
|
|
||||||
|
## [Bridge 2.4.7] Osney
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2078: Launcher inception.
|
||||||
|
* GODT-2039: fix --parent-pid flag is removed from command-line when restarting the application.
|
||||||
|
|
||||||
|
## [Bridge 2.4.6] Osney
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-2019: When signing out and a single user is connecte* we do not go back to the welcome screen.
|
||||||
|
* GODT-2071: Bridge-gui report error if an orphan bridge is detected.
|
||||||
|
* GODT-2046: Bridge-gui log is included in optional archive sent with bug reports.
|
||||||
|
* GODT-2039: Bridge monitors bridge-gui via its PID.
|
||||||
|
* GODT-2038: Interrupt gRPC initialisation of bridge process terminates.
|
||||||
|
* Other: Added timestamp to bridge-gui logs.
|
||||||
|
* GODT-2035: Bridge-gui log includes Qt version info.
|
||||||
|
* GODT-2031: Updated bridge description.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Other: Fix make run-qt target for Darwin.
|
||||||
|
|
||||||
## [Bridge 2.4.5] Osney
|
## [Bridge 2.4.5] Osney
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
6
Makefile
6
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=2.4.5+git
|
BRIDGE_APP_VERSION?=2.4.8+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
@ -296,7 +296,11 @@ run: run-qt
|
|||||||
run-cli: run-nogui
|
run-cli: run-nogui
|
||||||
|
|
||||||
run-qt: build-gui
|
run-qt: build-gui
|
||||||
|
ifeq "${TARGET_OS}" "darwin"
|
||||||
|
PROTONMAIL_ENV=dev ${DARWINAPP_CONTENTS}/MacOS/${LAUNCHER_EXE} ${RUN_FLAGS}
|
||||||
|
else
|
||||||
PROTONMAIL_ENV=dev ./${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE} ${RUN_FLAGS}
|
PROTONMAIL_ENV=dev ./${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE} ${RUN_FLAGS}
|
||||||
|
endif
|
||||||
|
|
||||||
run-nogui: build-nogui clean-vendor gofiles
|
run-nogui: build-nogui clean-vendor gofiles
|
||||||
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
|
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
|
||||||
|
|||||||
@ -55,6 +55,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() { //nolint:funlen
|
func main() { //nolint:funlen
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
l := logrus.WithField("launcher_version", constants.Version)
|
||||||
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
|
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
|
||||||
|
|
||||||
crashHandler := crash.NewHandler(reporter.ReportException)
|
crashHandler := crash.NewHandler(reporter.ReportException)
|
||||||
@ -62,58 +64,69 @@ func main() { //nolint:funlen
|
|||||||
|
|
||||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Failed to get locations provider")
|
l.WithError(err).Fatal("Failed to get locations provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
locations := locations.New(locationsProvider, configName)
|
locations := locations.New(locationsProvider, configName)
|
||||||
|
|
||||||
logsPath, err := locations.ProvideLogsPath()
|
logsPath, err := locations.ProvideLogsPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Failed to get logs path")
|
l.WithError(err).Fatal("Failed to get logs path")
|
||||||
}
|
}
|
||||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||||
|
|
||||||
if err := logging.Init(logsPath); err != nil {
|
if err := logging.Init(logsPath); err != nil {
|
||||||
logrus.WithError(err).Fatal("Failed to setup logging")
|
l.WithError(err).Fatal("Failed to setup logging")
|
||||||
}
|
}
|
||||||
|
|
||||||
logging.SetLevel(os.Getenv("VERBOSITY"))
|
logging.SetLevel(os.Getenv("VERBOSITY"))
|
||||||
|
|
||||||
updatesPath, err := locations.ProvideUpdatesPath()
|
updatesPath, err := locations.ProvideUpdatesPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Failed to get updates path")
|
l.WithError(err).Fatal("Failed to get updates path")
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Failed to create new verification key")
|
l.WithError(err).Fatal("Failed to create new verification key")
|
||||||
}
|
}
|
||||||
|
|
||||||
kr, err := crypto.NewKeyRing(key)
|
kr, err := crypto.NewKeyRing(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Failed to create new verification keyring")
|
l.WithError(err).Fatal("Failed to create new verification keyring")
|
||||||
}
|
}
|
||||||
|
|
||||||
versioner := versioner.New(updatesPath)
|
versioner := versioner.New(updatesPath)
|
||||||
|
|
||||||
exeToLaunch := guiName
|
|
||||||
args := os.Args[1:]
|
|
||||||
if inCLIMode(args) {
|
|
||||||
exeToLaunch = exeName
|
|
||||||
}
|
|
||||||
|
|
||||||
exe, err := getPathToUpdatedExecutable(exeToLaunch, versioner, kr, reporter)
|
|
||||||
if err != nil {
|
|
||||||
if exe, err = getFallbackExecutable(exeToLaunch, versioner); err != nil {
|
|
||||||
logrus.WithError(err).Fatal("Failed to find any launchable executable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
launcher, err := os.Executable()
|
launcher, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Failed to determine path to launcher")
|
logrus.WithError(err).Fatal("Failed to determine path to launcher")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l = l.WithField("launcher_path", launcher)
|
||||||
|
|
||||||
|
args := os.Args[1:]
|
||||||
|
|
||||||
|
exe, err := getPathToUpdatedExecutable(filepath.Base(launcher), versioner, kr, reporter)
|
||||||
|
if err != nil {
|
||||||
|
exeToLaunch := guiName
|
||||||
|
if inCLIMode(args) {
|
||||||
|
exeToLaunch = exeName
|
||||||
|
}
|
||||||
|
|
||||||
|
l = l.WithField("exe_to_launch", exeToLaunch)
|
||||||
|
l.WithError(err).Info("No more updates found, looking up bridge executable")
|
||||||
|
|
||||||
|
path, err := versioner.GetExecutableInDirectory(exeToLaunch, filepath.Dir(launcher))
|
||||||
|
if err != nil {
|
||||||
|
l.WithError(err).Fatal("No executable in launcher directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
exe = path
|
||||||
|
}
|
||||||
|
|
||||||
|
l = l.WithField("exe_path", exe)
|
||||||
|
|
||||||
args, wait, mainExe := findAndStripWait(args)
|
args, wait, mainExe := findAndStripWait(args)
|
||||||
if wait {
|
if wait {
|
||||||
waitForProcessToFinish(mainExe)
|
waitForProcessToFinish(mainExe)
|
||||||
@ -134,7 +147,7 @@ func main() { //nolint:funlen
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Failed to launch")
|
l.WithError(err).Fatal("Failed to launch")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,11 +206,11 @@ func findAndStripWait(args []string) ([]string, bool, string) {
|
|||||||
|
|
||||||
func getPathToUpdatedExecutable(
|
func getPathToUpdatedExecutable(
|
||||||
name string,
|
name string,
|
||||||
versioner *versioner.Versioner,
|
ver *versioner.Versioner,
|
||||||
kr *crypto.KeyRing,
|
kr *crypto.KeyRing,
|
||||||
reporter *sentry.Reporter,
|
reporter *sentry.Reporter,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
versions, err := versioner.ListVersions()
|
versions, err := ver.ListVersions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "failed to list available versions")
|
return "", errors.Wrap(err, "failed to list available versions")
|
||||||
}
|
}
|
||||||
@ -208,7 +221,11 @@ func getPathToUpdatedExecutable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, version := range versions {
|
for _, version := range versions {
|
||||||
vlog := logrus.WithField("version", version)
|
vlog := logrus.WithFields(logrus.Fields{
|
||||||
|
"version": constants.Version,
|
||||||
|
"check_version": version,
|
||||||
|
"name": name,
|
||||||
|
})
|
||||||
|
|
||||||
if err := version.VerifyFiles(kr); err != nil {
|
if err := version.VerifyFiles(kr); err != nil {
|
||||||
vlog.WithError(err).Error("Files failed verification and will be removed")
|
vlog.WithError(err).Error("Files failed verification and will be removed")
|
||||||
@ -241,17 +258,6 @@ func getPathToUpdatedExecutable(
|
|||||||
return "", errors.New("no available newer versions")
|
return "", errors.New("no available newer 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
// waitForProcessToFinish waits until the process with the given path is finished.
|
// waitForProcessToFinish waits until the process with the given path is finished.
|
||||||
func waitForProcessToFinish(exePath string) {
|
func waitForProcessToFinish(exePath string) {
|
||||||
for {
|
for {
|
||||||
|
|||||||
2
dist/info.rc
vendored
2
dist/info.rc
vendored
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
IDI_ICON1 ICON DISCARDABLE STRINGIZE(ICO_FILE)
|
IDI_ICON1 ICON DISCARDABLE STRINGIZE(ICO_FILE)
|
||||||
|
|
||||||
#define FILE_COMMENTS "The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer."
|
#define FILE_COMMENTS "Proton Mail Bridge is a desktop application that runs in the background, encrypting and decrypting messages as they enter and leave your computer."
|
||||||
#define FILE_DESCRIPTION "Proton Mail Bridge"
|
#define FILE_DESCRIPTION "Proton Mail Bridge"
|
||||||
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
|
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
|
||||||
#define PRODUCT_NAME "Proton Mail Bridge for Windows"
|
#define PRODUCT_NAME "Proton Mail Bridge for Windows"
|
||||||
|
|||||||
2
dist/proton-bridge.desktop
vendored
2
dist/proton-bridge.desktop
vendored
@ -3,7 +3,7 @@ Type=Application
|
|||||||
Version=1.1
|
Version=1.1
|
||||||
Name=Proton Mail Bridge
|
Name=Proton Mail Bridge
|
||||||
GenericName=Proton Mail Bridge for Linux
|
GenericName=Proton Mail Bridge for Linux
|
||||||
Comment=The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer.
|
Comment=Proton Mail Bridge is a desktop application that runs in the background, encrypting and decrypting messages as they enter and leave your computer.
|
||||||
Icon=protonmail-bridge
|
Icon=protonmail-bridge
|
||||||
Exec=protonmail-bridge
|
Exec=protonmail-bridge
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|||||||
@ -76,6 +76,7 @@ const (
|
|||||||
flagRestart = "restart"
|
flagRestart = "restart"
|
||||||
FlagLauncher = "launcher"
|
FlagLauncher = "launcher"
|
||||||
FlagNoWindow = "no-window"
|
FlagNoWindow = "no-window"
|
||||||
|
FlagParentPID = "parent-pid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Base struct {
|
type Base struct {
|
||||||
@ -324,6 +325,12 @@ func (b *Base) NewApp(mainLoop func(*Base, *cli.Context) error) *cli.App {
|
|||||||
Usage: "The launcher to use to restart the application",
|
Usage: "The launcher to use to restart the application",
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
},
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: FlagParentPID,
|
||||||
|
Usage: "The PID of the process that started the application. Ignored if frontend is not gRPC",
|
||||||
|
Hidden: true,
|
||||||
|
Value: -1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@ -20,6 +20,7 @@ package base
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/sys/execabs"
|
"golang.org/x/sys/execabs"
|
||||||
@ -38,6 +39,8 @@ func (b *Base) restartApp(crash bool) error {
|
|||||||
args = os.Args[1:]
|
args = os.Args[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args = removeFlagWithValue(args, FlagParentPID)
|
||||||
|
|
||||||
if b.launcher != "" {
|
if b.launcher != "" {
|
||||||
args = forceLauncherFlag(args, b.launcher)
|
args = forceLauncherFlag(args, b.launcher)
|
||||||
}
|
}
|
||||||
@ -85,6 +88,30 @@ func incrementRestartFlag(args []string) []string {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeFlagWithValue removes a flag that requires a value from a list of command line parameters.
|
||||||
|
// The flag can be of the following form `-flag value`, `--flag value`, `-flag=value` or `--flags=value`.
|
||||||
|
func removeFlagWithValue(argList []string, flag string) []string {
|
||||||
|
var result []string
|
||||||
|
|
||||||
|
for i := 0; i < len(argList); i++ {
|
||||||
|
arg := argList[i]
|
||||||
|
// "detect the parameter form "-flag value" or "--paramName value"
|
||||||
|
if (arg == "-"+flag) || (arg == "--"+flag) {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// "detect the form "--flag=value" or "--flag=value"
|
||||||
|
if strings.HasPrefix(arg, "-"+flag+"=") || (strings.HasPrefix(arg, "--"+flag+"=")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// forceLauncherFlag replace or add the launcher args with the one set in the app.
|
// forceLauncherFlag replace or add the launcher args with the one set in the app.
|
||||||
func forceLauncherFlag(args []string, launcher string) []string {
|
func forceLauncherFlag(args []string, launcher string) []string {
|
||||||
res := append([]string{}, args...)
|
res := append([]string{}, args...)
|
||||||
|
|||||||
@ -61,3 +61,22 @@ func TestVersionLessThan(t *testing.T) {
|
|||||||
r.False(current.LessThan(current))
|
r.False(current.LessThan(current))
|
||||||
r.False(newer.LessThan(current))
|
r.False(newer.LessThan(current))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemoveFlagWithValue(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
argList []string
|
||||||
|
flag string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{[]string{}, "b", nil},
|
||||||
|
{[]string{"-a", "-b=value", "-c"}, "b", []string{"-a", "-c"}},
|
||||||
|
{[]string{"-a", "--b=value", "-c"}, "b", []string{"-a", "-c"}},
|
||||||
|
{[]string{"-a", "-b", "value", "-c"}, "b", []string{"-a", "-c"}},
|
||||||
|
{[]string{"-a", "--b", "value", "-c"}, "b", []string{"-a", "-c"}},
|
||||||
|
{[]string{"-a", "-B=value", "-c"}, "b", []string{"-a", "-B=value", "-c"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
require.Equal(t, removeFlagWithValue(tt.argList, tt.flag), tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -86,6 +86,7 @@ func main(b *base.Base, c *cli.Context) error { //nolint:funlen
|
|||||||
b.Updater,
|
b.Updater,
|
||||||
b,
|
b,
|
||||||
b.Locations,
|
b.Locations,
|
||||||
|
c.Int(base.FlagParentPID),
|
||||||
)
|
)
|
||||||
|
|
||||||
cache, cacheErr := loadMessageCache(b)
|
cache, cacheErr := loadMessageCache(b)
|
||||||
|
|||||||
@ -39,7 +39,7 @@ const (
|
|||||||
var ErrSizeTooLarge = errors.New("file is too big")
|
var ErrSizeTooLarge = errors.New("file is too big")
|
||||||
|
|
||||||
// ReportBug reports a new bug from the user.
|
// ReportBug reports a new bug from the user.
|
||||||
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string, attachLogs bool) error {
|
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string, attachLogs bool) error { //nolint:funlen
|
||||||
if user, err := b.GetUser(address); err == nil {
|
if user, err := b.GetUser(address); err == nil {
|
||||||
accountName = user.Username()
|
accountName = user.Username()
|
||||||
} else if users := b.GetUsers(); len(users) > 0 {
|
} else if users := b.GetUsers(); len(users) > 0 {
|
||||||
@ -65,6 +65,16 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("Can't get log files list")
|
log.WithError(err).Error("Can't get log files list")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guiLogs, err := b.getMatchingLogs(
|
||||||
|
func(filename string) bool {
|
||||||
|
return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("Can't get GUI log files list")
|
||||||
|
}
|
||||||
|
|
||||||
crashes, err := b.getMatchingLogs(
|
crashes, err := b.getMatchingLogs(
|
||||||
func(filename string) bool {
|
func(filename string) bool {
|
||||||
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
||||||
@ -78,6 +88,10 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address,
|
|||||||
|
|
||||||
matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...)
|
matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...)
|
||||||
matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...)
|
matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...)
|
||||||
|
if len(guiLogs) > 0 {
|
||||||
|
// bridge-gui is keeping only one log file and it's small (~ 1kb), so we include it regardless of file count
|
||||||
|
matchFiles = append(matchFiles, guiLogs[len(guiLogs)-1])
|
||||||
|
}
|
||||||
|
|
||||||
archive, err := zipFiles(matchFiles)
|
archive, err := zipFiles(matchFiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -49,7 +49,7 @@ void QMLBackend::init(GRPCConfig const &serviceConfig)
|
|||||||
this->connectGrpcEvents();
|
this->connectGrpcEvents();
|
||||||
|
|
||||||
QString error;
|
QString error;
|
||||||
if (app().grpc().connectToServer(serviceConfig, error))
|
if (app().grpc().connectToServer(serviceConfig, app().bridgeMonitor(), error))
|
||||||
app().log().info("Connected to backend via gRPC service.");
|
app().log().info("Connected to backend via gRPC service.");
|
||||||
else
|
else
|
||||||
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
|
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
|
||||||
|
|||||||
@ -8,7 +8,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
BLOCK "040904b0"
|
BLOCK "040904b0"
|
||||||
BEGIN
|
BEGIN
|
||||||
VALUE "Comments", "The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer."
|
VALUE "Comments", "Proton Mail Bridge is a desktop application that runs in the background, encrypting and decrypting messages as they enter and leave your computer."
|
||||||
VALUE "CompanyName", "${BRIDGE_VENDOR}"
|
VALUE "CompanyName", "${BRIDGE_VENDOR}"
|
||||||
VALUE "FileDescription", "${BRIDGE_APP_FULL_NAME}"
|
VALUE "FileDescription", "${BRIDGE_APP_FULL_NAME}"
|
||||||
VALUE "FileVersion", "${BRIDGE_APP_VERSION_COMMA}"
|
VALUE "FileVersion", "${BRIDGE_APP_VERSION_COMMA}"
|
||||||
|
|||||||
@ -32,19 +32,33 @@ using namespace bridgepp;
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
/// \brief The file extension for the bridge executable file.
|
/// \brief The file extension for the bridge executable file.
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
QString const exeSuffix = ".exe";
|
QString const exeSuffix = ".exe";
|
||||||
#else
|
#else
|
||||||
QString const exeSuffix;
|
QString const exeSuffix;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QString const bridgeLock = "bridge-gui.lock"; ///< file name used for the lock file.
|
QString const bridgeLock = "bridge-gui.lock"; ///< file name used for the lock file.
|
||||||
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||||
qint64 const grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
qint64 const grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// According to Qt doc, one per application is OK, but its use should be restricted to a
|
||||||
|
/// single thread.
|
||||||
|
/// \return The network access manager for the application.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QNetworkAccessManager& networkManager()
|
||||||
|
{
|
||||||
|
static QNetworkAccessManager nam;
|
||||||
|
return nam;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The path of the bridge executable.
|
/// \return The path of the bridge executable.
|
||||||
/// \return A null string if the executable could not be located.
|
/// \return A null string if the executable could not be located.
|
||||||
@ -92,6 +106,14 @@ Log &initLog()
|
|||||||
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error))
|
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error))
|
||||||
log.error(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;
|
return log;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,21 +200,44 @@ QUrl getApiUrl()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return The URL for the focus endpoint of the bridge API URL.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QUrl getFocusUrl()
|
||||||
|
{
|
||||||
|
QUrl url = getApiUrl();
|
||||||
|
url.setPath("/focus");
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return true if an instance of bridge is already running.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool isBridgeRunning()
|
||||||
|
{
|
||||||
|
QTimer timer;
|
||||||
|
timer.setSingleShot(true);
|
||||||
|
|
||||||
|
std::unique_ptr<QNetworkReply> reply(networkManager().get(QNetworkRequest(getFocusUrl())));
|
||||||
|
QEventLoop loop;
|
||||||
|
bool timedOut = false;
|
||||||
|
QObject::connect(&timer, &QTimer::timeout, [&]() { timedOut = true; loop.quit(); });
|
||||||
|
QObject::connect(reply.get(), &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||||
|
timer.start(1000); // we time out after 1 second and consider no other instance is running.
|
||||||
|
loop.exec();
|
||||||
|
return ((!timedOut) && (reply->error() == QNetworkReply::NetworkError::NoError));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \brief Use api to bring focus on existing bridge instance.
|
/// \brief Use api to bring focus on existing bridge instance.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void focusOtherInstance()
|
void focusOtherInstance()
|
||||||
{
|
{
|
||||||
QNetworkAccessManager *manager;
|
std::unique_ptr<QNetworkReply> reply(networkManager().get(QNetworkRequest(getFocusUrl())));
|
||||||
QNetworkRequest request;
|
|
||||||
manager = new QNetworkAccessManager();
|
|
||||||
QUrl url = getApiUrl();
|
|
||||||
url.setPath("/focus");
|
|
||||||
request.setUrl(url);
|
|
||||||
QNetworkReply* rep = manager->get(request);
|
|
||||||
|
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
QObject::connect(rep, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
QObject::connect(reply.get(), &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||||
loop.exec();
|
loop.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +257,10 @@ void launchBridge(QStringList const &args)
|
|||||||
else
|
else
|
||||||
app().log().debug(QString("Bridge executable path: %1").arg(QDir::toNativeSeparators(bridgeExePath)));
|
app().log().debug(QString("Bridge executable path: %1").arg(QDir::toNativeSeparators(bridgeExePath)));
|
||||||
|
|
||||||
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, QStringList("--grpc") + args, nullptr), nullptr);
|
qint64 const pid = qApp->applicationPid();
|
||||||
|
QStringList const params = QStringList { "--grpc", "--parent-pid", QString::number(pid) } + args ;
|
||||||
|
app().log().info(QString("Launching bridge process with command \"%1\" %2").arg(bridgeExePath, params.join(" ")));
|
||||||
|
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, params , nullptr), nullptr);
|
||||||
overseer->startWorker(true);
|
overseer->startWorker(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,8 +307,8 @@ int main(int argc, char *argv[])
|
|||||||
{
|
{
|
||||||
focusOtherInstance();
|
focusOtherInstance();
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList args;
|
QStringList args;
|
||||||
QString launcher;
|
QString launcher;
|
||||||
bool attach = false;
|
bool attach = false;
|
||||||
@ -274,13 +322,16 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
if (!attach)
|
if (!attach)
|
||||||
{
|
{
|
||||||
|
if (isBridgeRunning())
|
||||||
|
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.");
|
||||||
|
|
||||||
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
|
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
|
||||||
GRPCClient::removeServiceConfigFile();
|
GRPCClient::removeServiceConfigFile();
|
||||||
launchBridge(args);
|
launchBridge(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
|
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
|
||||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(attach ? 0 : grpcServiceConfigWaitDelayMs));
|
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
|
||||||
if (!attach)
|
if (!attach)
|
||||||
GRPCClient::removeServiceConfigFile();
|
GRPCClient::removeServiceConfigFile();
|
||||||
|
|
||||||
@ -304,7 +355,7 @@ int main(int argc, char *argv[])
|
|||||||
if (bridgeMonitor)
|
if (bridgeMonitor)
|
||||||
{
|
{
|
||||||
const ProcessMonitor::MonitorStatus& status = bridgeMonitor->getStatus();
|
const ProcessMonitor::MonitorStatus& status = bridgeMonitor->getStatus();
|
||||||
if (!status.running && !attach)
|
if (status.ended && !attach)
|
||||||
{
|
{
|
||||||
// ProcessMonitor already stopped meaning we are attached to an orphan Bridge.
|
// ProcessMonitor already stopped meaning we are attached to an orphan Bridge.
|
||||||
// Restart the full process to be sure there is no more bridge orphans
|
// Restart the full process to be sure there is no more bridge orphans
|
||||||
|
|||||||
@ -98,7 +98,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
property bool _showSetup: false
|
property bool _showSetup: false
|
||||||
currentIndex: {
|
currentIndex: {
|
||||||
// show welcome when there are no users or only one non-logged-in user is present
|
// show welcome when there are no users
|
||||||
if (Backend.users.count === 0) {
|
if (Backend.users.count === 0) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@ -112,7 +112,8 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Backend.users.count === 1 && u.loggedIn === false) {
|
if (Backend.users.count === 1 && u.loggedIn === false) {
|
||||||
return 1
|
showSignIn(u.username)
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentLayout._showSetup) {
|
if (contentLayout._showSetup) {
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
#include "GRPCClient.h"
|
#include "GRPCClient.h"
|
||||||
#include "GRPCUtils.h"
|
#include "GRPCUtils.h"
|
||||||
#include "../Exception/Exception.h"
|
#include "../Exception/Exception.h"
|
||||||
|
#include "../ProcessMonitor.h"
|
||||||
|
|
||||||
|
|
||||||
using namespace google::protobuf;
|
using namespace google::protobuf;
|
||||||
@ -56,9 +57,10 @@ void GRPCClient::removeServiceConfigFile()
|
|||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] timeoutMs The timeout in milliseconds
|
/// \param[in] timeoutMs The timeout in milliseconds
|
||||||
|
/// \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.
|
/// \return The service config.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs)
|
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMonitor *serverProcess)
|
||||||
{
|
{
|
||||||
QString const path = grpcServerConfigPath();
|
QString const path = grpcServerConfigPath();
|
||||||
QFile file(path);
|
QFile file(path);
|
||||||
@ -68,6 +70,9 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs)
|
|||||||
bool found = false;
|
bool found = false;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
if (serverProcess && serverProcess->getStatus().ended)
|
||||||
|
throw Exception("Bridge application exited before providing a gRPC service configuration file.");
|
||||||
|
|
||||||
if (file.exists())
|
if (file.exists())
|
||||||
{
|
{
|
||||||
found = true;
|
found = true;
|
||||||
@ -100,9 +105,10 @@ void GRPCClient::setLog(Log *log)
|
|||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[out] outError If the function returns false, this variable contains a description of the error.
|
/// \param[out] outError If the function returns false, this variable contains a description of the error.
|
||||||
|
/// \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.
|
/// \return true iff the connection was successful.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
|
bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess, QString &outError)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -123,6 +129,9 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
|
|||||||
int i = 0;
|
int i = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
if (serverProcess && serverProcess->getStatus().ended)
|
||||||
|
throw Exception("Bridge application ended before gRPC connexion could be established.");
|
||||||
|
|
||||||
this->logInfo(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i));
|
this->logInfo(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i));
|
||||||
|
|
||||||
if (channel_->WaitForConnected(gpr_time_add(gpr_now(GPR_CLOCK_REALTIME), gpr_time_from_millis(grpcConnectionRetryDelayMs, GPR_TIMESPAN))))
|
if (channel_->WaitForConnected(gpr_time_add(gpr_now(GPR_CLOCK_REALTIME), gpr_time_from_millis(grpcConnectionRetryDelayMs, GPR_TIMESPAN))))
|
||||||
|
|||||||
@ -50,7 +50,7 @@ class GRPCClient : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public: // static member functions
|
public: // static member functions
|
||||||
static void removeServiceConfigFile(); ///< Delete the service config file.
|
static void removeServiceConfigFile(); ///< Delete the service config file.
|
||||||
static GRPCConfig waitAndRetrieveServiceConfig(qint64 timeoutMs); ///< Wait and retrieve the service configuration.
|
static GRPCConfig waitAndRetrieveServiceConfig(qint64 timeoutMs, class ProcessMonitor *serverProcess); ///< Wait and retrieve the service configuration.
|
||||||
|
|
||||||
public: // member functions.
|
public: // member functions.
|
||||||
GRPCClient() = default; ///< Default constructor.
|
GRPCClient() = default; ///< Default constructor.
|
||||||
@ -60,7 +60,7 @@ public: // member functions.
|
|||||||
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
|
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
|
||||||
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
|
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
|
||||||
void setLog(Log *log); ///< Set the log for the client.
|
void setLog(Log *log); ///< Set the log for the client.
|
||||||
bool connectToServer(GRPCConfig const &config, QString &outError); ///< Establish connection to the gRPC server.
|
bool connectToServer(GRPCConfig const &config, class ProcessMonitor *serverProcess, QString &outError); ///< Establish connection to the gRPC server.
|
||||||
|
|
||||||
grpc::Status checkTokens(QString const &clientConfigPath, QString &outReturnedClientToken); ///< Performs a token check.
|
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.
|
grpc::Status addLogEntry(Log::Level level, QString const &package, QString const &message); ///< Performs the "AddLogEntry" gRPC call.
|
||||||
|
|||||||
@ -30,7 +30,7 @@ namespace
|
|||||||
Log *qtHandlerLog { nullptr }; ///< The log instance handling qt logs.
|
Log *qtHandlerLog { nullptr }; ///< The log instance handling qt logs.
|
||||||
QMutex qtHandlerMutex; ///< A mutex used to access qtHandlerLog.
|
QMutex qtHandlerMutex; ///< A mutex used to access qtHandlerLog.
|
||||||
|
|
||||||
// Mapping of log levels to string. Maybe used to lookup using both side a a key, so a list of pair is more convenient that a map.
|
// Mapping of log levels to string. Maybe used to lookup using both side a key, so a list of pair is more convenient that a map.
|
||||||
QList<QPair<Log::Level, QString>> const logLevelStrings {
|
QList<QPair<Log::Level, QString>> const logLevelStrings {
|
||||||
{ Log::Level::Panic, "panic", },
|
{ Log::Level::Panic, "panic", },
|
||||||
{ Log::Level::Fatal, "fatal", },
|
{ Log::Level::Fatal, "fatal", },
|
||||||
@ -96,15 +96,15 @@ void qtMessageHandler(QtMsgType type, QMessageLogContext const &, QString const
|
|||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \brief return a string representing the log entry
|
/// \brief return a string representing the log entry, in a format similar to the one used by logrus.
|
||||||
///
|
///
|
||||||
/// \param[in] level The log entry level.
|
/// \param[in] level The log entry level.
|
||||||
/// \param[in] message The log entry message.
|
/// \param[in] message The log entry message.
|
||||||
/// \return The string for the log entry
|
/// \return The string for the log entry
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
QString Log::logEntryToString(Log::Level level, QString const &message)
|
QString Log::logEntryToString(Log::Level level, QDateTime const &dateTime, QString const &message)
|
||||||
{
|
{
|
||||||
return QString("[%1] %2").arg(levelToString(level).toUpper(), message);
|
return QString("%1[%2] %3").arg(levelToString(level).left(4).toUpper(), dateTime.toString("MMM dd HH:mm:ss.zzz"), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -303,15 +303,17 @@ void Log::trace(QString const &message)
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void Log::addEntry(Log::Level level, QString const &message)
|
void Log::addEntry(Log::Level level, QString const &message)
|
||||||
{
|
{
|
||||||
|
QDateTime const dateTime = QDateTime::currentDateTime();
|
||||||
QMutexLocker locker(&mutex_);
|
QMutexLocker locker(&mutex_);
|
||||||
if (qint32(level) > qint32(level_))
|
if (qint32(level) > qint32(level_))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
emit entryAdded(level, message);
|
emit entryAdded(level, message);
|
||||||
|
|
||||||
if (!(echoInConsole_ || file_))
|
if (!(echoInConsole_ || file_))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
QString const entryStr = logEntryToString(level, message) + "\n";
|
QString const entryStr = logEntryToString(level, dateTime, message) + "\n";
|
||||||
if (echoInConsole_)
|
if (echoInConsole_)
|
||||||
{
|
{
|
||||||
QTextStream &stream = (qint32(level) <= (qint32(Level::Warn))) ? stderr_ : stdout_;
|
QTextStream &stream = (qint32(level) <= (qint32(Level::Warn))) ? stderr_ : stdout_;
|
||||||
|
|||||||
@ -44,7 +44,7 @@ public: // data types.
|
|||||||
};
|
};
|
||||||
|
|
||||||
public: // static member functions.
|
public: // static member functions.
|
||||||
static QString logEntryToString(Log::Level level, QString const &message); ///< Return a string describing a log entry.
|
static QString logEntryToString(Log::Level level, QDateTime const &dateTime, QString const &message); ///< Return a string describing a log entry.
|
||||||
static QString levelToString(Log::Level level); ///< return the string for a level.
|
static QString levelToString(Log::Level level); ///< return the string for a level.
|
||||||
static bool stringToLevel(QString const &str, Log::Level& outLevel); ///< parse a level from a string.
|
static bool stringToLevel(QString const &str, Log::Level& outLevel); ///< parse a level from a string.
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,8 @@ ProcessMonitor::ProcessMonitor(QString const &exePath, QStringList const &args,
|
|||||||
: Worker(parent)
|
: Worker(parent)
|
||||||
, exePath_(exePath)
|
, exePath_(exePath)
|
||||||
, args_(args)
|
, args_(args)
|
||||||
|
, out_(stdout)
|
||||||
|
, err_(stderr)
|
||||||
{
|
{
|
||||||
QFileInfo fileInfo(exePath);
|
QFileInfo fileInfo(exePath);
|
||||||
if (!fileInfo.exists())
|
if (!fileInfo.exists())
|
||||||
@ -42,6 +44,26 @@ ProcessMonitor::ProcessMonitor(QString const &exePath, QStringList const &args,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
//
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void ProcessMonitor::forwardProcessOutput(QProcess &p) {
|
||||||
|
QByteArray array = p.readAllStandardError();
|
||||||
|
if (!array.isEmpty())
|
||||||
|
{
|
||||||
|
err_ << array;
|
||||||
|
err_.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
array = p.readAllStandardOutput();
|
||||||
|
if (!array.isEmpty())
|
||||||
|
{
|
||||||
|
out_ << array;
|
||||||
|
out_.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -49,35 +71,31 @@ void ProcessMonitor::run()
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&statusMutex_);
|
||||||
|
status_.ended = false;
|
||||||
|
status_.pid = -1;
|
||||||
|
}
|
||||||
|
|
||||||
emit started();
|
emit started();
|
||||||
|
|
||||||
QProcess p;
|
QProcess p;
|
||||||
p.start(exePath_, args_);
|
p.start(exePath_, args_);
|
||||||
p.waitForStarted();
|
p.waitForStarted();
|
||||||
|
|
||||||
status_.running = true;
|
|
||||||
status_.pid = p.processId();
|
|
||||||
|
|
||||||
QTextStream out(stdout), err(stderr);
|
|
||||||
QByteArray array;
|
|
||||||
while (!p.waitForFinished(100))
|
|
||||||
{
|
{
|
||||||
array = p.readAllStandardError();
|
QMutexLocker locker(&statusMutex_);
|
||||||
if (!array.isEmpty())
|
status_.pid = p.processId();
|
||||||
{
|
|
||||||
err << array;
|
|
||||||
err.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
array = p.readAllStandardOutput();
|
|
||||||
if (!array.isEmpty())
|
|
||||||
{
|
|
||||||
out << array;
|
|
||||||
out.flush();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
status_.running = false;
|
while (!p.waitForFinished(100))
|
||||||
|
{
|
||||||
|
this->forwardProcessOutput(p);
|
||||||
|
}
|
||||||
|
this->forwardProcessOutput(p);
|
||||||
|
|
||||||
|
QMutexLocker locker(&statusMutex_);
|
||||||
|
status_.ended = true;
|
||||||
status_.returnCode = p.exitCode();
|
status_.returnCode = p.exitCode();
|
||||||
|
|
||||||
emit processExited(status_.returnCode);
|
emit processExited(status_.returnCode);
|
||||||
@ -93,8 +111,9 @@ void ProcessMonitor::run()
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return status of the monitored process
|
/// \return status of the monitored process
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
const ProcessMonitor::MonitorStatus &ProcessMonitor::getStatus()
|
const ProcessMonitor::MonitorStatus ProcessMonitor::getStatus()
|
||||||
{
|
{
|
||||||
|
QMutexLocker locker(&statusMutex_);
|
||||||
return status_;
|
return status_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ Q_OBJECT
|
|||||||
public: // static member functions
|
public: // static member functions
|
||||||
struct MonitorStatus
|
struct MonitorStatus
|
||||||
{
|
{
|
||||||
bool running = false;
|
bool ended = false;
|
||||||
int returnCode = 0;
|
int returnCode = 0;
|
||||||
qint64 pid = 0;
|
qint64 pid = 0;
|
||||||
};
|
};
|
||||||
@ -49,15 +49,21 @@ public: // member functions.
|
|||||||
ProcessMonitor &operator=(ProcessMonitor const &) = delete; ///< Disabled assignment operator.
|
ProcessMonitor &operator=(ProcessMonitor const &) = delete; ///< Disabled assignment operator.
|
||||||
ProcessMonitor &operator=(ProcessMonitor &&) = delete; ///< Disabled move assignment operator.
|
ProcessMonitor &operator=(ProcessMonitor &&) = delete; ///< Disabled move assignment operator.
|
||||||
void run() override; ///< Run the worker.
|
void run() override; ///< Run the worker.
|
||||||
MonitorStatus const &getStatus();
|
MonitorStatus const getStatus(); ///< Retrieve the current status of the process.
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void processExited(int code); ///< Slot for the exiting of the process.
|
void processExited(int code); ///< Slot for the exiting of the process.
|
||||||
|
|
||||||
|
private: // member functions
|
||||||
|
void forwardProcessOutput(QProcess &p); ///< Forward the standard output and error from the process to this application standard output and error.
|
||||||
|
|
||||||
private: // data members
|
private: // data members
|
||||||
|
QMutex statusMutex_; ///< The status mutex.
|
||||||
QString const exePath_; ///< The path to the executable.
|
QString const exePath_; ///< The path to the executable.
|
||||||
QStringList args_; ///< arguments to be passed to the brigde.
|
QStringList args_; ///< arguments to be passed to the brigde.
|
||||||
MonitorStatus status_; ///< Status of the monitoring.
|
MonitorStatus status_; ///< Status of the monitoring.
|
||||||
|
QTextStream out_; ///< The standard output stream.
|
||||||
|
QTextStream err_; ///< The standard error stream.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,7 @@ func New(
|
|||||||
updater types.Updater,
|
updater types.Updater,
|
||||||
restarter types.Restarter,
|
restarter types.Restarter,
|
||||||
locations *locations.Locations,
|
locations *locations.Locations,
|
||||||
|
parentPID int,
|
||||||
) Frontend {
|
) Frontend {
|
||||||
switch frontendType {
|
switch frontendType {
|
||||||
case GRPC:
|
case GRPC:
|
||||||
@ -66,6 +67,7 @@ func New(
|
|||||||
updater,
|
updater,
|
||||||
restarter,
|
restarter,
|
||||||
locations,
|
locations,
|
||||||
|
parentPID,
|
||||||
)
|
)
|
||||||
|
|
||||||
case CLI:
|
case CLI:
|
||||||
|
|||||||
@ -40,6 +40,9 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
|
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
||||||
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/elastic/go-sysinfo"
|
||||||
|
sysinfotypes "github.com/elastic/go-sysinfo/types"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -85,6 +88,8 @@ type Service struct { // nolint:structcheck
|
|||||||
locations *locations.Locations
|
locations *locations.Locations
|
||||||
token string
|
token string
|
||||||
pemCert string
|
pemCert string
|
||||||
|
parentPID int
|
||||||
|
parentPIDDoneCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService returns a new instance of the service.
|
// NewService returns a new instance of the service.
|
||||||
@ -95,6 +100,7 @@ func NewService(
|
|||||||
updater types.Updater,
|
updater types.Updater,
|
||||||
restarter types.Restarter,
|
restarter types.Restarter,
|
||||||
locations *locations.Locations,
|
locations *locations.Locations,
|
||||||
|
parentPID int,
|
||||||
) *Service {
|
) *Service {
|
||||||
s := Service{
|
s := Service{
|
||||||
UnimplementedBridgeServer: UnimplementedBridgeServer{},
|
UnimplementedBridgeServer: UnimplementedBridgeServer{},
|
||||||
@ -110,6 +116,8 @@ func NewService(
|
|||||||
firstTimeAutostart: sync.Once{},
|
firstTimeAutostart: sync.Once{},
|
||||||
locations: locations,
|
locations: locations,
|
||||||
token: uuid.NewString(),
|
token: uuid.NewString(),
|
||||||
|
parentPID: parentPID,
|
||||||
|
parentPIDDoneCh: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initializing.Done is only called sync.Once. Please keep the increment
|
// Initializing.Done is only called sync.Once. Please keep the increment
|
||||||
@ -177,6 +185,12 @@ func (s *Service) Loop(b types.Bridger) error {
|
|||||||
s.initAutostart()
|
s.initAutostart()
|
||||||
s.startGRPCServer()
|
s.startGRPCServer()
|
||||||
|
|
||||||
|
if s.parentPID < 0 {
|
||||||
|
s.log.Info("Not monitoring parent PID")
|
||||||
|
} else {
|
||||||
|
go s.monitorParentPID()
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
s.bridge.SetBool(settings.FirstStartGUIKey, false)
|
s.bridge.SetBool(settings.FirstStartGUIKey, false)
|
||||||
}()
|
}()
|
||||||
@ -520,3 +534,36 @@ func (s *Service) validateStreamServerToken(
|
|||||||
|
|
||||||
return handler(srv, ss)
|
return handler(srv, ss)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// monitorParentPID check at regular intervals that the parent process is still alive, and if not shuts down the server
|
||||||
|
// and the applications.
|
||||||
|
func (s *Service) monitorParentPID() {
|
||||||
|
s.log.Infof("Starting to monitor parent PID %v", s.parentPID)
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if s.parentPID < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
processes, err := sysinfo.Processes() // sysinfo.Process(pid) does not seem to work on Windows.
|
||||||
|
if err != nil {
|
||||||
|
s.log.Debug("Could not retrieve process list")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !xslices.Any(processes, func(p sysinfotypes.Process) bool { return p != nil && p.PID() == s.parentPID }) {
|
||||||
|
s.log.Info("Parent process does not exist anymore. Initiating shutdown")
|
||||||
|
go s.quit() // quit will write to the parentPIDDoneCh, so we launch a goroutine.
|
||||||
|
} else {
|
||||||
|
s.log.Tracef("Parent process %v is still alive", s.parentPID)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-s.parentPIDDoneCh:
|
||||||
|
s.log.Infof("Stopping process monitoring for PID %v", s.parentPID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -95,15 +95,19 @@ func (s *Service) GuiReady(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empt
|
|||||||
// Quit implement the Quit gRPC service call.
|
// Quit implement the Quit gRPC service call.
|
||||||
func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
||||||
s.log.Debug("Quit")
|
s.log.Debug("Quit")
|
||||||
return &emptypb.Empty{}, s.quit()
|
s.quit()
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) quit() error {
|
func (s *Service) quit() {
|
||||||
// Windows is notably slow at Quitting. We do it in a goroutine to speed things up a bit.
|
// Windows is notably slow at Quitting. We do it in a goroutine to speed things up a bit.
|
||||||
go func() {
|
go func() {
|
||||||
var err error
|
if s.parentPID >= 0 {
|
||||||
|
s.parentPIDDoneCh <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
if s.isStreamingEvents() {
|
if s.isStreamingEvents() {
|
||||||
if err = s.stopEventStream(); err != nil {
|
if err := s.stopEventStream(); err != nil {
|
||||||
s.log.WithError(err).Error("Quit failed.")
|
s.log.WithError(err).Error("Quit failed.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,8 +115,6 @@ func (s *Service) quit() error {
|
|||||||
// The following call is launched as a goroutine, as it will wait for current calls to end, including this one.
|
// The following call is launched as a goroutine, as it will wait for current calls to end, including this one.
|
||||||
s.grpcServer.GracefulStop()
|
s.grpcServer.GracefulStop()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart implement the Restart gRPC service call.
|
// Restart implement the Restart gRPC service call.
|
||||||
|
|||||||
@ -71,8 +71,9 @@ func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunE
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case <-server.Context().Done():
|
case <-server.Context().Done():
|
||||||
s.log.Debug("Client closed the stream, exiting")
|
s.log.Info("Client closed the stream, initiating shutdown")
|
||||||
return s.quit()
|
s.quit()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -101,3 +101,7 @@ func getLogName(version, revision string) string {
|
|||||||
func MatchLogName(name string) bool {
|
func MatchLogName(name string) bool {
|
||||||
return regexp.MustCompile(`^v.*\.log$`).MatchString(name)
|
return regexp.MustCompile(`^v.*\.log$`).MatchString(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MatchGUILogName(name string) bool {
|
||||||
|
return regexp.MustCompile(`^gui_v.*\.log$`).MatchString(name)
|
||||||
|
}
|
||||||
|
|||||||
@ -17,6 +17,12 @@
|
|||||||
|
|
||||||
package versioner
|
package versioner
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
func getExeName(name string) string {
|
func getExeName(name string) string {
|
||||||
|
if strings.HasSuffix(name, ".exe") {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
return name + ".exe"
|
return name + ".exe"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.PHONY: check-go check-godog install-godog test test-bridge test-live test-live-bridge test-stage test-debug test-live-debug bench
|
.PHONY: check-go check-godog install-godog test test-bridge test-live test-live-bridge test-stage test-debug test-live-debug bench
|
||||||
|
|
||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
export BRIDGE_VERSION:=2.4.5+integrationtests
|
export BRIDGE_VERSION:=2.4.8+integrationtests
|
||||||
export VERBOSITY?=fatal
|
export VERBOSITY?=fatal
|
||||||
export TEST_DATA=testdata
|
export TEST_DATA=testdata
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user