fix(BRIDGE-8): more robust command-line args parser in bridge-gui.

fix(BRIDGE-8): add command-line invocation to log.
This commit is contained in:
Xavier Michelon
2024-04-10 14:30:18 +02:00
parent bb15efa711
commit c692c21b87
7 changed files with 194 additions and 135 deletions

View File

@ -21,9 +21,7 @@ import (
"testing" "testing"
"github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/bradenaw/juniper/xslices"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/exp/slices"
) )
func TestFindAndStrip(t *testing.T) { func TestFindAndStrip(t *testing.T) {
@ -31,53 +29,53 @@ func TestFindAndStrip(t *testing.T) {
result, found := findAndStrip(list, "a") result, found := findAndStrip(list, "a")
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"b", "c", "c", "b", "c"})) assert.Equal(t, result, []string{"b", "c", "c", "b", "c"})
result, found = findAndStrip(list, "c") result, found = findAndStrip(list, "c")
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"a", "b", "b"})) assert.Equal(t, result, []string{"a", "b", "b"})
result, found = findAndStrip([]string{"c", "c", "c"}, "c") result, found = findAndStrip([]string{"c", "c", "c"}, "c")
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{})) assert.Equal(t, result, []string{})
result, found = findAndStrip(list, "A") result, found = findAndStrip(list, "A")
assert.False(t, found) assert.False(t, found)
assert.True(t, xslices.Equal(result, list)) assert.Equal(t, result, list)
result, found = findAndStrip([]string{}, "a") result, found = findAndStrip([]string{}, "a")
assert.False(t, found) assert.False(t, found)
assert.True(t, xslices.Equal(result, []string{})) assert.Equal(t, result, []string{})
} }
func TestFindAndStripWait(t *testing.T) { func TestFindAndStripWait(t *testing.T) {
result, found, values := findAndStripWait([]string{"a", "b", "c"}) result, found, values := findAndStripWait([]string{"a", "b", "c"})
assert.False(t, found) assert.False(t, found)
assert.True(t, xslices.Equal(result, []string{"a", "b", "c"})) assert.Equal(t, result, []string{"a", "b", "c"})
assert.True(t, xslices.Equal(values, []string{})) assert.Equal(t, values, []string{})
result, found, values = findAndStripWait([]string{"a", "--wait", "b"}) result, found, values = findAndStripWait([]string{"a", "--wait", "b"})
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"a"})) assert.Equal(t, result, []string{"a"})
assert.True(t, xslices.Equal(values, []string{"b"})) assert.Equal(t, values, []string{"b"})
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c"}) result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c"})
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"a"})) assert.Equal(t, result, []string{"a"})
assert.True(t, xslices.Equal(values, []string{"b", "c"})) assert.Equal(t, values, []string{"b", "c"})
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c", "--wait", "d"}) result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c", "--wait", "d"})
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"a"})) assert.Equal(t, result, []string{"a"})
assert.True(t, xslices.Equal(values, []string{"b", "c", "d"})) assert.Equal(t, values, []string{"b", "c", "d"})
} }
func TestAppendOrModifySessionID(t *testing.T) { func TestAppendOrModifySessionID(t *testing.T) {
sessionID := string(logging.NewSessionID()) sessionID := string(logging.NewSessionID())
assert.True(t, slices.Equal(appendOrModifySessionID(nil, sessionID), []string{"--session-id", sessionID})) assert.Equal(t, appendOrModifySessionID(nil, sessionID), []string{"--session-id", sessionID})
assert.True(t, slices.Equal(appendOrModifySessionID([]string{}, sessionID), []string{"--session-id", sessionID})) assert.Equal(t, appendOrModifySessionID([]string{}, sessionID), []string{"--session-id", sessionID})
assert.True(t, slices.Equal(appendOrModifySessionID([]string{"--cli"}, sessionID), []string{"--cli", "--session-id", sessionID})) assert.Equal(t, appendOrModifySessionID([]string{"--cli"}, sessionID), []string{"--cli", "--session-id", sessionID})
assert.True(t, slices.Equal(appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})) assert.Equal(t, appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})
assert.True(t, slices.Equal(appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})) assert.Equal(t, appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})
assert.True(t, slices.Equal(appendOrModifySessionID([]string{"--session-id", "<oldID>", "--cli"}, sessionID), []string{"--session-id", sessionID, "--cli"})) assert.Equal(t, appendOrModifySessionID([]string{"--session-id", "<oldID>", "--cli"}, sessionID), []string{"--session-id", sessionID, "--cli"})
} }

View File

@ -15,113 +15,104 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "Pch.h" #include "Pch.h"
#include "CommandLine.h" #include "CommandLine.h"
#include "Settings.h" #include "Settings.h"
#include <bridgepp/CLI/CLIUtils.h>
#include <bridgepp/SessionID/SessionID.h> #include <bridgepp/SessionID/SessionID.h>
using namespace bridgepp; using namespace bridgepp;
namespace { namespace {
QString const hyphenatedLauncherFlag = "--launcher"; ///< launcher flag parameter used for bridge.
QString const launcherFlag = "--launcher"; ///< launcher flag parameter used for bridge. QString const hyphenatedWindowFlag = "--no-window"; ///< The no-window command-line flag.
QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag. QString const hyphenatedSoftwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag. enable software rendering for a single execution
QString const softwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag. enable software rendering for a single execution QString const hyphenatedSetSoftwareRendererFlag = "--set-software-renderer"; ///< The 'set-software-renderer' command-line flag. Software rendering will be used for all subsequent executions of the application.
QString const setSoftwareRendererFlag = "--set-software-renderer"; ///< The 'set-software-renderer' command-line flag. Software rendering will be used for all subsequent executions of the application. QString const hyphenatedSetHardwareRendererFlag = "--set-hardware-renderer"; ///< The 'set-hardware-renderer' command-line flag. Hardware rendering will be used for all subsequent executions of the application.
QString const setHardwareRendererFlag = "--set-hardware-renderer"; ///< The 'set-hardware-renderer' command-line flag. Hardware rendering will be used for all subsequent executions of the application. QString const sessionIDFlag = "session-id";
QString const hyphenatedSessionIDFlag = "--" + sessionIDFlag;
//****************************************************************************************************************************************************
/// \brief parse a command-line string argument as expected by go's CLI package.
/// \param[in] argc The number of arguments passed to the application.
/// \param[in] argv The list of arguments passed to the application.
/// \param[in] paramNames the list of names for the parameter
//****************************************************************************************************************************************************
QString parseGoCLIStringArgument(int argc, char *argv[], QStringList paramNames) {
// go cli package is pretty permissive when it comes to parsing arguments. For each name 'param', all the following seems to be accepted:
// -param value
// --param value
// -param=value
// --param=value
for (QString const &paramName: paramNames) {
for (qsizetype i = 1; i < argc; ++i) {
QString const arg(QString::fromLocal8Bit(argv[i]));
if ((i < argc - 1) && ((arg == "-" + paramName) || (arg == "--" + paramName))) {
return QString(argv[i + 1]);
}
QRegularExpressionMatch match = QRegularExpression(QString("^-{1,2}%1=(.+)$").arg(paramName)).match(arg);
if (match.hasMatch()) {
return match.captured(1);
}
}
}
return QString();
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \brief Parse the log level from the command-line arguments. /// \brief Parse the log level from the command-line arguments.
/// ///
/// \param[in] argc The number of arguments passed to the application. /// \param[in] args The command-line arguments.
/// \param[in] argv The list of arguments passed to the application.
/// \return The log level. if not specified on the command-line, the default log level is returned. /// \return The log level. if not specified on the command-line, the default log level is returned.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
Log::Level parseLogLevel(int argc, char *argv[]) { Log::Level parseLogLevel(QStringList const &args) {
QString levelStr = parseGoCLIStringArgument(argc, argv, { "l", "log-level" }); QStringList levelStr = parseGoCLIStringArgument(args, {"l", "log-level"});
if (levelStr.isEmpty()) { if (levelStr.isEmpty()) {
return Log::defaultLevel; return Log::defaultLevel;
} }
Log::Level level = Log::defaultLevel; Log::Level level = Log::defaultLevel;
Log::stringToLevel(levelStr, level); Log::stringToLevel(levelStr.back(), level);
return level; return level;
} }
//****************************************************************************************************************************************************
/// \brief Return the most recent sessionID parsed in command-line arguments
///
/// \param[in] args The command-line arguments.
/// \return The most recent sessionID in the list. If the list is empty, a new sessionID is created.
//****************************************************************************************************************************************************
QString mostRecentSessionID(QStringList const& args) {
QStringList const sessionIDs = parseGoCLIStringArgument(args, {sessionIDFlag});
if (sessionIDs.isEmpty()) {
return newSessionID();
}
return std::ranges::max(sessionIDs, [](QString const &lhs, QString const &rhs) -> bool {
return sessionIDToDateTime(lhs) < sessionIDToDateTime(rhs);
});
}
} // anonymous namespace } // anonymous namespace
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] argc number of arguments passed to the application. /// \param[in] argv list of arguments passed to the application, including the exe name/path at index 0.
/// \param[in] argv list of arguments passed to the application.
/// \return The parsed options. /// \return The parsed options.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
CommandLineOptions parseCommandLine(int argc, char *argv[]) { CommandLineOptions parseCommandLine(QStringList const &argv) {
CommandLineOptions options; CommandLineOptions options;
bool flagFound = false; bool launcherFlagFound = false;
options.launcher = QString::fromLocal8Bit(argv[0]); options.launcher = argv[0];
// for unknown reasons, on Windows QCoreApplication::arguments() frequently returns an empty list, which is incorrect, so we rebuild the argument // for unknown reasons, on Windows QCoreApplication::arguments() frequently returns an empty list, which is incorrect, so we rebuild the argument
// list from the original argc and argv values. // list from the original argc and argv values.
for (int i = 1; i < argc; i++) { for (int i = 1; i < argv.count(); i++) {
QString const &arg = QString::fromLocal8Bit(argv[i]); QString const &arg = argv[i];
// we can't use QCommandLineParser here since it will fail on unknown options. // we can't use QCommandLineParser here since it will fail on unknown options.
// we skip session-id for now we'll process it later, with a special treatment for duplicates
if (arg == hyphenatedSessionIDFlag) {
i++; // we skip the next param, which if the flag's value.
continue;
}
if (arg.startsWith(hyphenatedSessionIDFlag + "=")) {
continue;
}
// Arguments may contain some bridge flags. // Arguments may contain some bridge flags.
if (arg == softwareRendererFlag) { if (arg == hyphenatedSoftwareRendererFlag) {
options.bridgeGuiArgs.append(arg); options.bridgeGuiArgs.append(arg);
options.useSoftwareRenderer = true; options.useSoftwareRenderer = true;
} }
if (arg == setSoftwareRendererFlag) { if (arg == hyphenatedSetSoftwareRendererFlag) {
app().settings().setUseSoftwareRenderer(true); app().settings().setUseSoftwareRenderer(true);
continue; // setting is permanent. no need to keep/pass it to bridge for restart. continue; // setting is permanent. no need to keep/pass it to bridge for restart.
} }
if (arg == setHardwareRendererFlag) { if (arg == hyphenatedSetHardwareRendererFlag) {
app().settings().setUseSoftwareRenderer(false); app().settings().setUseSoftwareRenderer(false);
continue; // setting is permanent. no need to keep/pass it to bridge for restart. continue; // setting is permanent. no need to keep/pass it to bridge for restart.
} }
if (arg == noWindowFlag) { if (arg == hyphenatedWindowFlag) {
options.noWindow = true; options.noWindow = true;
} }
if (arg == launcherFlag) { if (arg == hyphenatedLauncherFlag) {
options.bridgeArgs.append(arg); options.bridgeArgs.append(arg);
options.launcher = QString::fromLocal8Bit(argv[++i]); options.launcher = argv[++i];
options.bridgeArgs.append(options.launcher); options.bridgeArgs.append(options.launcher);
flagFound = true; launcherFlagFound = true;
} }
#ifdef QT_DEBUG #ifdef QT_DEBUG
else if (arg == "--attach" || arg == "-a") { else if (arg == "--attach" || arg == "-a") {
@ -135,22 +126,24 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
options.bridgeGuiArgs.append(arg); options.bridgeGuiArgs.append(arg);
} }
} }
if (!flagFound) { if (!launcherFlagFound) {
// add bridge-gui as launcher // add bridge-gui as launcher
options.bridgeArgs.append(launcherFlag); options.bridgeArgs.append(hyphenatedLauncherFlag);
options.bridgeArgs.append(options.launcher); options.bridgeArgs.append(options.launcher);
} }
options.logLevel = parseLogLevel(argc, argv); QStringList args;
if (!argv.isEmpty()) {
QString sessionID = parseGoCLIStringArgument(argc, argv, { "session-id" }); args = argv.last(argv.count() - 1);
if (sessionID.isEmpty()) {
// The session ID was not passed to us on the command-line -> create one and add to the command-line for bridge
sessionID = newSessionID();
options.bridgeArgs.append("--session-id");
options.bridgeArgs.append(sessionID);
} }
options.logLevel = parseLogLevel(args);
QString const sessionID = mostRecentSessionID(args);
options.bridgeArgs.append(hyphenatedSessionIDFlag);
options.bridgeArgs.append(sessionID);
app().setSessionID(sessionID); app().setSessionID(sessionID);
return options; return options;
} }

View File

@ -37,7 +37,7 @@ struct CommandLineOptions {
}; };
CommandLineOptions parseCommandLine(int argc, char *argv[]); ///< Parse the command-line arguments CommandLineOptions parseCommandLine(QStringList const &argv); ///< Parse the command-line arguments
#endif //BRIDGE_GUI_COMMAND_LINE_H #endif //BRIDGE_GUI_COMMAND_LINE_H

View File

@ -15,7 +15,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "BridgeApp.h" #include "BridgeApp.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include "CommandLine.h" #include "CommandLine.h"
@ -30,13 +29,12 @@
#include <bridgepp/Log/LogUtils.h> #include <bridgepp/Log/LogUtils.h>
#include <bridgepp/ProcessMonitor.h> #include <bridgepp/ProcessMonitor.h>
#include "bridgepp/CLI/CLIUtils.h"
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
#include "MacOS/SecondInstance.h" #include "MacOS/SecondInstance.h"
#endif #endif
using namespace bridgepp; using namespace bridgepp;
@ -50,17 +48,14 @@ QString const exeSuffix = ".exe";
QString const exeSuffix; QString const exeSuffix;
#endif #endif
QString const bridgeLock = "bridge-v3.lock"; ///< The file name used for the bridge-gui lock file. QString const bridgeLock = "bridge-v3.lock"; ///< The file name used for the bridge-gui lock file.
QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for the bridge-gui lock file. QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for the bridge-gui 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 constexpr grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
QString const waitFlag = "--wait"; ///< The wait command-line flag. QString const waitFlag = "--wait"; ///< The wait command-line flag.
} // anonymous namespace } // 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.
@ -70,7 +65,6 @@ QString locateBridgeExe() {
return (fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable()) ? fileInfo.absoluteFilePath() : QString(); return (fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable()) ? fileInfo.absoluteFilePath() : QString();
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// // initialize the Qt application. /// // initialize the Qt application.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -97,8 +91,6 @@ void initQtApplication() {
#endif // #ifdef Q_OS_MACOS #endif // #ifdef Q_OS_MACOS
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] engine The QML component. /// \param[in] engine The QML component.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -118,13 +110,12 @@ QQmlComponent *createRootQmlComponent(QQmlApplicationEngine &engine) {
rootComponent->loadUrl(QUrl(qrcQmlDir + "/Bridge.qml")); rootComponent->loadUrl(QUrl(qrcQmlDir + "/Bridge.qml"));
if (rootComponent->status() != QQmlComponent::Status::Ready) { if (rootComponent->status() != QQmlComponent::Status::Ready) {
QString const &err = rootComponent->errorString(); QString const &err = rootComponent->errorString();
app().log().error(err); app().log().error(err);
throw Exception("Could not load QML component", err); throw Exception("Could not load QML component", err);
} }
return rootComponent; return rootComponent;
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] lock The lock file to be checked. /// \param[in] lock The lock file to be checked.
/// \return True if the lock can be taken, false otherwise. /// \return True if the lock can be taken, false otherwise.
@ -155,7 +146,6 @@ bool checkSingleInstance(QLockFile &lock) {
return true; return true;
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return QUrl to reach the bridge API. /// \return QUrl to reach the bridge API.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -184,7 +174,6 @@ QUrl getApiUrl() {
return url; return url;
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \brief Check if bridge is running. /// \brief Check if bridge is running.
/// ///
@ -199,7 +188,6 @@ bool isBridgeRunning() {
return (!lockFile.tryLock()) && (lockFile.error() == QLockFile::LockFailedError); return (!lockFile.tryLock()) && (lockFile.error() == QLockFile::LockFailedError);
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \brief Use api to bring focus on existing bridge instance. /// \brief Use api to bring focus on existing bridge instance.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -213,8 +201,7 @@ void focusOtherInstance() {
if (!sc.load(path)) { if (!sc.load(path)) {
throw Exception("The gRPC focus service configuration file is invalid."); throw Exception("The gRPC focus service configuration file is invalid.");
} }
} } else {
else {
throw Exception("Server did not provide gRPC Focus service configuration."); throw Exception("Server did not provide gRPC Focus service configuration.");
} }
@ -225,20 +212,18 @@ void focusOtherInstance() {
if (!client.raise("focusOtherInstance").ok()) { if (!client.raise("focusOtherInstance").ok()) {
throw Exception(QString("The raise call to the bridge focus service failed.")); throw Exception(QString("The raise call to the bridge focus service failed."));
} }
} } catch (Exception const &e) {
catch (Exception const &e) {
app().log().error(e.qwhat()); app().log().error(e.qwhat());
auto uuid = reportSentryException("Exception occurred during focusOtherInstance()", e); auto uuid = reportSentryException("Exception occurred during focusOtherInstance()", e);
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), e.qwhat())); app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), e.qwhat()));
} }
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param [in] args list of arguments to pass to bridge. /// \param [in] args list of arguments to pass to bridge.
/// \return bridge executable path /// \return bridge executable path
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
const QString launchBridge(QStringList const &args) { QString launchBridge(QStringList const &args) {
UPOverseer &overseer = app().bridgeOverseer(); UPOverseer &overseer = app().bridgeOverseer();
overseer.reset(); overseer.reset();
@ -251,26 +236,38 @@ const QString launchBridge(QStringList const &args) {
} }
qint64 const pid = qApp->applicationPid(); qint64 const pid = qApp->applicationPid();
QStringList const params = QStringList { "--grpc", "--parent-pid", QString::number(pid) } + args; 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(" "))); 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 = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, params, nullptr), nullptr);
overseer->startWorker(true); overseer->startWorker(true);
return bridgeExePath; return bridgeExePath;
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
// //
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void closeBridgeApp() { void closeBridgeApp() {
app().grpc().quit(); // this will cause the grpc service and the bridge app to close. app().grpc().quit(); // this will cause the grpc service and the bridge app to close.
UPOverseer &overseer = app().bridgeOverseer(); UPOverseer const &overseer = app().bridgeOverseer();
if (overseer) { // A null overseer means the app was run in 'attach' mode. We're not monitoring it. if (overseer) {
// A null overseer means the app was run in 'attach' mode. We're not monitoring it.
// ReSharper disable once CppExpressionWithoutSideEffects
overseer->wait(Overseer::maxTerminationWaitTimeMs); overseer->wait(Overseer::maxTerminationWaitTimeMs);
} }
} }
//****************************************************************************************************************************************************
/// \param[in] argv The command-line argments, including the application name at index 0.
//****************************************************************************************************************************************************
void logCommandLineInvocation(QStringList argv) {
Log &log = app().log();
if (argv.isEmpty()) {
log.error("The command line is empty");
}
log.info("bridge-gui executable: " + argv.front());
log.info("Command-line invocation: " + (argv.size() > 1 ? argv.last(argv.size() - 1).join(" ") : "<none>"));
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] argc The number of command-line arguments. /// \param[in] argc The number of command-line arguments.
@ -289,12 +286,11 @@ int main(int argc, char *argv[]) {
auto sentryCloser = qScopeGuard([] { sentry_close(); }); auto sentryCloser = qScopeGuard([] { sentry_close(); });
try { try {
QString const& configDir = bridgepp::userConfigDir(); QString const &configDir = bridgepp::userConfigDir();
initQtApplication(); initQtApplication();
QStringList const argvList = cliArgsToStringList(argc, argv);
CommandLineOptions const cliOptions = parseCommandLine(argc, argv); CommandLineOptions const cliOptions = parseCommandLine(argvList);
Log &log = initLog(); Log &log = initLog();
log.setLevel(cliOptions.logLevel); log.setLevel(cliOptions.logLevel);
@ -309,6 +305,8 @@ int main(int argc, char *argv[]) {
setDockIconVisibleState(!cliOptions.noWindow); setDockIconVisibleState(!cliOptions.noWindow);
#endif #endif
logCommandLineInvocation(argvList);
// In attached mode, we do not intercept stderr and stdout of bridge, as we did not launch it ourselves, so we output the log to the console. // In attached mode, we do not intercept stderr and stdout of bridge, as we did not launch it ourselves, so we output the log to the console.
// When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept // When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept
// these outputs and output them on the command-line. // these outputs and output them on the command-line.
@ -348,7 +346,6 @@ int main(int argc, char *argv[]) {
QQuickWindow::setSceneGraphBackend((app().settings().useSoftwareRenderer() || cliOptions.useSoftwareRenderer) ? "software" : "rhi"); QQuickWindow::setSceneGraphBackend((app().settings().useSoftwareRenderer() || cliOptions.useSoftwareRenderer) ? "software" : "rhi");
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend())); log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine)); std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext())); std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
@ -374,7 +371,7 @@ int main(int argc, char *argv[]) {
app().log().debug(QString("Monitoring Bridge PID : %1").arg(status.pid)); app().log().debug(QString("Monitoring Bridge PID : %1").arg(status.pid));
connection = QObject::connect(bridgeMonitor, &ProcessMonitor::processExited, [&](int returnCode) { connection = QObject::connect(bridgeMonitor, &ProcessMonitor::processExited, [&](int returnCode) {
bridgeExited = true;// clazy:exclude=lambda-in-connect bridgeExited = true; // clazy:exclude=lambda-in-connect
qGuiApp->exit(returnCode); qGuiApp->exit(returnCode);
}); });
} }
@ -383,7 +380,7 @@ int main(int argc, char *argv[]) {
int result = 0; int result = 0;
if (!startError) { if (!startError) {
// we succeeded in launching bridge, so we can be set as mainExecutable. // we succeeded in launching bridge, so we can be set as mainExecutable.
QString mainexec = QString::fromLocal8Bit(argv[0]); QString const mainexec = argvList[0];
app().grpc().setMainExecutable(mainexec); app().grpc().setMainExecutable(mainexec);
QStringList args = cliOptions.bridgeGuiArgs; QStringList args = cliOptions.bridgeGuiArgs;
args.append(waitFlag); args.append(waitFlag);
@ -412,8 +409,7 @@ int main(int argc, char *argv[]) {
// release the lock file // release the lock file
lock.unlock(); lock.unlock();
return result; return result;
} } catch (Exception const &e) {
catch (Exception const &e) {
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e); sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
QString message = e.qwhat(); QString message = e.qwhat();
if (e.showSupportLink()) { if (e.showSupportLink()) {

View File

@ -23,16 +23,15 @@
using namespace bridgepp; using namespace bridgepp;
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
// //
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
TEST(CLI, stripStringParameterFromCommandLine) { TEST(CLI, stripStringParameterFromCommandLine) {
struct Test { struct TestData {
QStringList input; QStringList input;
QStringList expectedOutput; QStringList expectedOutput;
}; };
QList<Test> const tests = { QList<TestData> const tests = {
{{}, {}}, {{}, {}},
{{ "--a", "-b", "--C" }, { "--a", "-b", "--C" } }, {{ "--a", "-b", "--C" }, { "--a", "-b", "--C" } },
{{ "--string", "value" }, {} }, {{ "--string", "value" }, {} },
@ -44,7 +43,36 @@ TEST(CLI, stripStringParameterFromCommandLine) {
{{ "--string", "--string", "value", "-b", "--string"}, { "value", "-b" } }, {{ "--string", "--string", "value", "-b", "--string"}, { "value", "-b" } },
}; };
for (Test const& test: tests) { for (TestData const& test: tests) {
EXPECT_EQ(stripStringParameterFromCommandLine("--string", test.input), test.expectedOutput); EXPECT_EQ(stripStringParameterFromCommandLine("--string", test.input), test.expectedOutput);
} }
} }
TEST(CLI, parseGoCLIStringArgument) {
struct TestData {
QStringList args;
QStringList params;
QStringList expectedOutput;
};
QList<TestData> const tests = {
{ {}, {}, {} },
{ {"-param"}, {"param"}, {} },
{ {"--param", "1"}, {"param"}, { "1" } },
{ {"--param", "1","p", "-p", "2", "-flag", "-param=3", "--p=4"}, {"param", "p"}, { "1", "2", "3", "4" } },
{ {"--param", "--param", "1"}, {"param"}, { "--param" } },
};
for (TestData const& test: tests) {
EXPECT_EQ(parseGoCLIStringArgument(test.args, test.params), test.expectedOutput);
}
}
TEST(CLI, cliArgsToStringList) {
int constexpr argc = 3;
char *argv[] = { const_cast<char *>("1"), const_cast<char *>("2"), const_cast<char *>("3") };
QStringList const strList { "1", "2", "3" };
EXPECT_EQ(cliArgsToStringList(argc,argv), strList);
EXPECT_EQ(cliArgsToStringList(0, nullptr), QStringList {});
}

View File

@ -42,4 +42,51 @@ QStringList stripStringParameterFromCommandLine(QString const &paramName, QStrin
} }
//****************************************************************************************************************************************************
/// The flags may be present more than once in the args. All values are returned in order of appearance.
///
/// \param[in] args The arguments
/// \param[in] paramNames the list of names for the parameter, without any prefix hypen.
/// \return The values found for the flag.
//****************************************************************************************************************************************************
QStringList parseGoCLIStringArgument(QStringList const &args, QStringList const& paramNames) {
// go cli package is pretty permissive when it comes to parsing arguments. For each name 'param', all the following seems to be accepted:
// -param value
// --param value
// -param=value
// --param=value
QStringList result;
qsizetype const argCount = args.count();
for (qsizetype i = 0; i < args.size(); ++i) {
for (QString const &paramName: paramNames) {
if ((i < argCount - 1) && ((args[i] == "-" + paramName) || (args[i] == "--" + paramName))) {
result.append(args[i + 1]);
i += 1;
continue;
}
if (QRegularExpressionMatch match = QRegularExpression(QString("^-{1,2}%1=(.+)$").arg(paramName)).match(args[i]); match.hasMatch()) {
result.append(match.captured(1));
continue;
}
}
}
return result;
}
//****************************************************************************************************************************************************
/// \param[in] argc The number of command-line arguments.
/// \param[in] argv The list of command-line arguments.
/// \return A QStringList representing the arguments list.
//****************************************************************************************************************************************************
QStringList cliArgsToStringList(int argc, char **argv) {
QStringList result;
result.reserve(argc);
for (qsizetype i = 0; i < argc; ++i) {
result.append(QString::fromLocal8Bit(argv[i]));
}
return result;
}
} // namespace bridgepp } // namespace bridgepp

View File

@ -15,18 +15,15 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#ifndef BRIDGEPP_CLI_UTILS_H #ifndef BRIDGEPP_CLI_UTILS_H
#define BRIDGEPP_CLI_UTILS_H #define BRIDGEPP_CLI_UTILS_H
namespace bridgepp { namespace bridgepp {
QStringList stripStringParameterFromCommandLine(QString const &paramName, QStringList const &commandLineParams); ///< Remove a string parameter from a list of command-line parameters. QStringList stripStringParameterFromCommandLine(QString const &paramName, QStringList const &commandLineParams); ///< Remove a string parameter from a list of command-line parameters.
QStringList parseGoCLIStringArgument(QStringList const &args, QStringList const &paramNames); ///< Parse a command-line string argument as expected by go's CLI package.
QStringList cliArgsToStringList(int argc, char **argv); ///< Converts C-style command-line arguments to a string list.
} }
#endif // BRIDGEPP_CLI_UTILS_H #endif // BRIDGEPP_CLI_UTILS_H