Compare commits

...

15 Commits

Author SHA1 Message Date
debe87f2f5 Other: Bridge Osney v2.4.8 2022-11-14 09:28:07 +01:00
cca2807256 GODT-2071: fix --no-window flag that was broken on Windows. 2022-11-11 21:34:56 +01:00
7b73f76e78 Other: Bridge Osney v2.4.7 2022-11-11 14:06:56 +01:00
b1eefd6c85 GODT-2078: Launcher inception. 2022-11-11 12:26:06 +01:00
bbcb7ad980 GODT-2039: fix --parent-pid flag is removed from command-line when restarting the application. 2022-11-11 11:10:27 +01:00
984c43cd75 Other: Bridge Osney v2.4.6 2022-11-10 15:41:33 +01:00
ec4c0fdd09 GODT-2019: when signing out and a single user is connected, we do not go back to the welcome screen. 2022-11-10 15:31:56 +01:00
51d4a9c7ee GODT-2071: bridge-gui report error if an orphan bridge is detected. 2022-11-10 15:31:55 +01:00
19930f63e2 GODT-2046: bridge-gui log is included in optional archive sent with bug reports. 2022-11-10 15:31:55 +01:00
3b9a3aaad2 GODT-2039: bridge monitors bridge-gui via its PID. 2022-11-10 15:31:55 +01:00
f5148074fd GODT-2038: interrupt gRPC initialisation of bridge process terminates. 2022-11-10 15:31:54 +01:00
a949a113cf Other: added timestamp to bridge-gui logs.
Changed format to be closer to logrus output.
2022-11-10 15:31:54 +01:00
227e9df419 GODT-2035: bridge-gui log includes Qt version info. 2022-11-10 15:31:53 +01:00
2a6d462be1 GODT-2031: updated bridge description. 2022-11-10 15:31:53 +01:00
bb03fa26cd Other: fix make run-qt target for Darwin. 2022-11-09 17:07:42 +01:00
27 changed files with 355 additions and 101 deletions

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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...)

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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 {

View File

@ -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));

View File

@ -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}"

View File

@ -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

View File

@ -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) {

View File

@ -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))))

View File

@ -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.

View File

@ -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_;

View File

@ -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.

View File

@ -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_;
} }

View File

@ -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.
}; };

View File

@ -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:

View File

@ -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
}
}
}

View File

@ -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.

View File

@ -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
} }
} }
} }

View File

@ -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)
}

View File

@ -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"
} }

View File

@ -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