mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 20:56:51 +00:00
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/)
|
||||
|
||||
## [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
|
||||
|
||||
### 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
|
||||
|
||||
# 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_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -296,7 +296,11 @@ run: run-qt
|
||||
run-cli: run-nogui
|
||||
|
||||
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}
|
||||
endif
|
||||
|
||||
run-nogui: build-nogui clean-vendor gofiles
|
||||
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
|
||||
|
||||
@ -55,6 +55,8 @@ const (
|
||||
)
|
||||
|
||||
func main() { //nolint:funlen
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
l := logrus.WithField("launcher_version", constants.Version)
|
||||
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
|
||||
|
||||
crashHandler := crash.NewHandler(reporter.ReportException)
|
||||
@ -62,58 +64,69 @@ func main() { //nolint:funlen
|
||||
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
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)
|
||||
|
||||
logsPath, err := locations.ProvideLogsPath()
|
||||
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))
|
||||
|
||||
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"))
|
||||
|
||||
updatesPath, err := locations.ProvideUpdatesPath()
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
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)
|
||||
if wait {
|
||||
waitForProcessToFinish(mainExe)
|
||||
@ -134,7 +147,7 @@ func main() { //nolint:funlen
|
||||
}
|
||||
|
||||
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(
|
||||
name string,
|
||||
versioner *versioner.Versioner,
|
||||
ver *versioner.Versioner,
|
||||
kr *crypto.KeyRing,
|
||||
reporter *sentry.Reporter,
|
||||
) (string, error) {
|
||||
versions, err := versioner.ListVersions()
|
||||
versions, err := ver.ListVersions()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to list available versions")
|
||||
}
|
||||
@ -208,7 +221,11 @@ func getPathToUpdatedExecutable(
|
||||
}
|
||||
|
||||
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 {
|
||||
vlog.WithError(err).Error("Files failed verification and will be removed")
|
||||
@ -241,17 +258,6 @@ func getPathToUpdatedExecutable(
|
||||
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.
|
||||
func waitForProcessToFinish(exePath string) {
|
||||
for {
|
||||
|
||||
2
dist/info.rc
vendored
2
dist/info.rc
vendored
@ -3,7 +3,7 @@
|
||||
|
||||
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 INTERNAL_NAME STRINGIZE(EXE_NAME)
|
||||
#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
|
||||
Name=Proton Mail Bridge
|
||||
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
|
||||
Exec=protonmail-bridge
|
||||
Terminal=false
|
||||
|
||||
@ -76,6 +76,7 @@ const (
|
||||
flagRestart = "restart"
|
||||
FlagLauncher = "launcher"
|
||||
FlagNoWindow = "no-window"
|
||||
FlagParentPID = "parent-pid"
|
||||
)
|
||||
|
||||
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",
|
||||
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
|
||||
|
||||
@ -20,6 +20,7 @@ package base
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/execabs"
|
||||
@ -38,6 +39,8 @@ func (b *Base) restartApp(crash bool) error {
|
||||
args = os.Args[1:]
|
||||
}
|
||||
|
||||
args = removeFlagWithValue(args, FlagParentPID)
|
||||
|
||||
if b.launcher != "" {
|
||||
args = forceLauncherFlag(args, b.launcher)
|
||||
}
|
||||
@ -85,6 +88,30 @@ func incrementRestartFlag(args []string) []string {
|
||||
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.
|
||||
func forceLauncherFlag(args []string, launcher string) []string {
|
||||
res := append([]string{}, args...)
|
||||
|
||||
@ -61,3 +61,22 @@ func TestVersionLessThan(t *testing.T) {
|
||||
r.False(current.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,
|
||||
b.Locations,
|
||||
c.Int(base.FlagParentPID),
|
||||
)
|
||||
|
||||
cache, cacheErr := loadMessageCache(b)
|
||||
|
||||
@ -39,7 +39,7 @@ const (
|
||||
var ErrSizeTooLarge = errors.New("file is too big")
|
||||
|
||||
// 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 {
|
||||
accountName = user.Username()
|
||||
} else if users := b.GetUsers(); len(users) > 0 {
|
||||
@ -65,6 +65,16 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address,
|
||||
if err != nil {
|
||||
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(
|
||||
func(filename string) bool {
|
||||
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, 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)
|
||||
if err != nil {
|
||||
|
||||
@ -49,7 +49,7 @@ void QMLBackend::init(GRPCConfig const &serviceConfig)
|
||||
this->connectGrpcEvents();
|
||||
|
||||
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.");
|
||||
else
|
||||
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
|
||||
|
||||
@ -8,7 +8,7 @@ BEGIN
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
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 "FileDescription", "${BRIDGE_APP_FULL_NAME}"
|
||||
VALUE "FileVersion", "${BRIDGE_APP_VERSION_COMMA}"
|
||||
|
||||
@ -32,19 +32,33 @@ using namespace bridgepp;
|
||||
namespace
|
||||
{
|
||||
|
||||
/// \brief The file extension for the bridge executable file.
|
||||
/// \brief The file extension for the bridge executable file.
|
||||
#ifdef Q_OS_WIN32
|
||||
QString const exeSuffix = ".exe";
|
||||
#else
|
||||
QString const exeSuffix;
|
||||
#endif
|
||||
|
||||
QString const bridgeLock = "bridge-gui.lock"; ///< file name used for the lock file.
|
||||
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||
qint64 const grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
||||
QString const bridgeLock = "bridge-gui.lock"; ///< file name used for the lock file.
|
||||
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||
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 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))
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
//****************************************************************************************************************************************************
|
||||
void focusOtherInstance()
|
||||
{
|
||||
QNetworkAccessManager *manager;
|
||||
QNetworkRequest request;
|
||||
manager = new QNetworkAccessManager();
|
||||
QUrl url = getApiUrl();
|
||||
url.setPath("/focus");
|
||||
request.setUrl(url);
|
||||
QNetworkReply* rep = manager->get(request);
|
||||
|
||||
std::unique_ptr<QNetworkReply> reply(networkManager().get(QNetworkRequest(getFocusUrl())));
|
||||
QEventLoop loop;
|
||||
QObject::connect(rep, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
QObject::connect(reply.get(), &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
@ -212,7 +257,10 @@ void launchBridge(QStringList const &args)
|
||||
else
|
||||
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);
|
||||
}
|
||||
|
||||
@ -259,8 +307,8 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
focusOtherInstance();
|
||||
return EXIT_FAILURE;
|
||||
|
||||
}
|
||||
|
||||
QStringList args;
|
||||
QString launcher;
|
||||
bool attach = false;
|
||||
@ -274,13 +322,16 @@ int main(int argc, char *argv[])
|
||||
|
||||
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.
|
||||
GRPCClient::removeServiceConfigFile();
|
||||
launchBridge(args);
|
||||
}
|
||||
|
||||
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)
|
||||
GRPCClient::removeServiceConfigFile();
|
||||
|
||||
@ -304,7 +355,7 @@ int main(int argc, char *argv[])
|
||||
if (bridgeMonitor)
|
||||
{
|
||||
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.
|
||||
// Restart the full process to be sure there is no more bridge orphans
|
||||
|
||||
@ -98,7 +98,7 @@ ApplicationWindow {
|
||||
|
||||
property bool _showSetup: false
|
||||
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) {
|
||||
return 1
|
||||
}
|
||||
@ -112,7 +112,8 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
if (Backend.users.count === 1 && u.loggedIn === false) {
|
||||
return 1
|
||||
showSignIn(u.username)
|
||||
return 0
|
||||
}
|
||||
|
||||
if (contentLayout._showSetup) {
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
#include "GRPCClient.h"
|
||||
#include "GRPCUtils.h"
|
||||
#include "../Exception/Exception.h"
|
||||
#include "../ProcessMonitor.h"
|
||||
|
||||
|
||||
using namespace google::protobuf;
|
||||
@ -56,9 +57,10 @@ void GRPCClient::removeServiceConfigFile()
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \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.
|
||||
//****************************************************************************************************************************************************
|
||||
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs)
|
||||
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMonitor *serverProcess)
|
||||
{
|
||||
QString const path = grpcServerConfigPath();
|
||||
QFile file(path);
|
||||
@ -68,6 +70,9 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs)
|
||||
bool found = false;
|
||||
while (true)
|
||||
{
|
||||
if (serverProcess && serverProcess->getStatus().ended)
|
||||
throw Exception("Bridge application exited before providing a gRPC service configuration file.");
|
||||
|
||||
if (file.exists())
|
||||
{
|
||||
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[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.
|
||||
//****************************************************************************************************************************************************
|
||||
bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
|
||||
bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess, QString &outError)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -123,6 +129,9 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
|
||||
int i = 0;
|
||||
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));
|
||||
|
||||
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
|
||||
public: // static member functions
|
||||
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.
|
||||
GRPCClient() = default; ///< Default constructor.
|
||||
@ -60,7 +60,7 @@ public: // member functions.
|
||||
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
|
||||
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
|
||||
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 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.
|
||||
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 {
|
||||
{ Log::Level::Panic, "panic", },
|
||||
{ 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] message The log entry message.
|
||||
/// \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)
|
||||
{
|
||||
QDateTime const dateTime = QDateTime::currentDateTime();
|
||||
QMutexLocker locker(&mutex_);
|
||||
if (qint32(level) > qint32(level_))
|
||||
return;
|
||||
|
||||
emit entryAdded(level, message);
|
||||
|
||||
if (!(echoInConsole_ || file_))
|
||||
return;
|
||||
|
||||
QString const entryStr = logEntryToString(level, message) + "\n";
|
||||
QString const entryStr = logEntryToString(level, dateTime, message) + "\n";
|
||||
if (echoInConsole_)
|
||||
{
|
||||
QTextStream &stream = (qint32(level) <= (qint32(Level::Warn))) ? stderr_ : stdout_;
|
||||
|
||||
@ -44,7 +44,7 @@ public: // data types.
|
||||
};
|
||||
|
||||
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 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)
|
||||
, exePath_(exePath)
|
||||
, args_(args)
|
||||
, out_(stdout)
|
||||
, err_(stderr)
|
||||
{
|
||||
QFileInfo fileInfo(exePath);
|
||||
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
|
||||
{
|
||||
{
|
||||
QMutexLocker locker(&statusMutex_);
|
||||
status_.ended = false;
|
||||
status_.pid = -1;
|
||||
}
|
||||
|
||||
emit started();
|
||||
|
||||
QProcess p;
|
||||
p.start(exePath_, args_);
|
||||
p.waitForStarted();
|
||||
|
||||
status_.running = true;
|
||||
status_.pid = p.processId();
|
||||
|
||||
QTextStream out(stdout), err(stderr);
|
||||
QByteArray array;
|
||||
while (!p.waitForFinished(100))
|
||||
{
|
||||
array = p.readAllStandardError();
|
||||
if (!array.isEmpty())
|
||||
{
|
||||
err << array;
|
||||
err.flush();
|
||||
}
|
||||
|
||||
array = p.readAllStandardOutput();
|
||||
if (!array.isEmpty())
|
||||
{
|
||||
out << array;
|
||||
out.flush();
|
||||
}
|
||||
QMutexLocker locker(&statusMutex_);
|
||||
status_.pid = p.processId();
|
||||
}
|
||||
|
||||
status_.running = false;
|
||||
while (!p.waitForFinished(100))
|
||||
{
|
||||
this->forwardProcessOutput(p);
|
||||
}
|
||||
this->forwardProcessOutput(p);
|
||||
|
||||
QMutexLocker locker(&statusMutex_);
|
||||
status_.ended = true;
|
||||
status_.returnCode = p.exitCode();
|
||||
|
||||
emit processExited(status_.returnCode);
|
||||
@ -93,8 +111,9 @@ void ProcessMonitor::run()
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return status of the monitored process
|
||||
//****************************************************************************************************************************************************
|
||||
const ProcessMonitor::MonitorStatus &ProcessMonitor::getStatus()
|
||||
const ProcessMonitor::MonitorStatus ProcessMonitor::getStatus()
|
||||
{
|
||||
QMutexLocker locker(&statusMutex_);
|
||||
return status_;
|
||||
}
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ Q_OBJECT
|
||||
public: // static member functions
|
||||
struct MonitorStatus
|
||||
{
|
||||
bool running = false;
|
||||
bool ended = false;
|
||||
int returnCode = 0;
|
||||
qint64 pid = 0;
|
||||
};
|
||||
@ -49,15 +49,21 @@ public: // member functions.
|
||||
ProcessMonitor &operator=(ProcessMonitor const &) = delete; ///< Disabled assignment operator.
|
||||
ProcessMonitor &operator=(ProcessMonitor &&) = delete; ///< Disabled move assignment operator.
|
||||
void run() override; ///< Run the worker.
|
||||
MonitorStatus const &getStatus();
|
||||
MonitorStatus const getStatus(); ///< Retrieve the current status of the process.
|
||||
|
||||
signals:
|
||||
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
|
||||
QMutex statusMutex_; ///< The status mutex.
|
||||
QString const exePath_; ///< The path to the executable.
|
||||
QStringList args_; ///< arguments to be passed to the brigde.
|
||||
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,
|
||||
restarter types.Restarter,
|
||||
locations *locations.Locations,
|
||||
parentPID int,
|
||||
) Frontend {
|
||||
switch frontendType {
|
||||
case GRPC:
|
||||
@ -66,6 +67,7 @@ func New(
|
||||
updater,
|
||||
restarter,
|
||||
locations,
|
||||
parentPID,
|
||||
)
|
||||
|
||||
case CLI:
|
||||
|
||||
@ -40,6 +40,9 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
|
||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||
"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/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -85,6 +88,8 @@ type Service struct { // nolint:structcheck
|
||||
locations *locations.Locations
|
||||
token string
|
||||
pemCert string
|
||||
parentPID int
|
||||
parentPIDDoneCh chan struct{}
|
||||
}
|
||||
|
||||
// NewService returns a new instance of the service.
|
||||
@ -95,6 +100,7 @@ func NewService(
|
||||
updater types.Updater,
|
||||
restarter types.Restarter,
|
||||
locations *locations.Locations,
|
||||
parentPID int,
|
||||
) *Service {
|
||||
s := Service{
|
||||
UnimplementedBridgeServer: UnimplementedBridgeServer{},
|
||||
@ -110,6 +116,8 @@ func NewService(
|
||||
firstTimeAutostart: sync.Once{},
|
||||
locations: locations,
|
||||
token: uuid.NewString(),
|
||||
parentPID: parentPID,
|
||||
parentPIDDoneCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
// 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.startGRPCServer()
|
||||
|
||||
if s.parentPID < 0 {
|
||||
s.log.Info("Not monitoring parent PID")
|
||||
} else {
|
||||
go s.monitorParentPID()
|
||||
}
|
||||
|
||||
defer func() {
|
||||
s.bridge.SetBool(settings.FirstStartGUIKey, false)
|
||||
}()
|
||||
@ -520,3 +534,36 @@ func (s *Service) validateStreamServerToken(
|
||||
|
||||
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.
|
||||
func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
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.
|
||||
go func() {
|
||||
var err error
|
||||
if s.parentPID >= 0 {
|
||||
s.parentPIDDoneCh <- struct{}{}
|
||||
}
|
||||
|
||||
if s.isStreamingEvents() {
|
||||
if err = s.stopEventStream(); err != nil {
|
||||
if err := s.stopEventStream(); err != nil {
|
||||
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.
|
||||
s.grpcServer.GracefulStop()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restart implement the Restart gRPC service call.
|
||||
|
||||
@ -71,8 +71,9 @@ func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunE
|
||||
return err
|
||||
}
|
||||
case <-server.Context().Done():
|
||||
s.log.Debug("Client closed the stream, exiting")
|
||||
return s.quit()
|
||||
s.log.Info("Client closed the stream, initiating shutdown")
|
||||
s.quit()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,3 +101,7 @@ func getLogName(version, revision string) string {
|
||||
func MatchLogName(name string) bool {
|
||||
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
|
||||
|
||||
import "strings"
|
||||
|
||||
func getExeName(name string) string {
|
||||
if strings.HasSuffix(name, ".exe") {
|
||||
return name
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
export GO111MODULE=on
|
||||
export BRIDGE_VERSION:=2.4.5+integrationtests
|
||||
export BRIDGE_VERSION:=2.4.8+integrationtests
|
||||
export VERBOSITY?=fatal
|
||||
export TEST_DATA=testdata
|
||||
|
||||
|
||||
Reference in New Issue
Block a user