FEAT(server,client): use CLI11 for cli parsing

Implements #6063
This commit is contained in:
Thomas Windt 2025-04-19 18:07:32 +02:00 committed by Robert Adam
parent f5778ec615
commit 10cd98ecf7
9 changed files with 468 additions and 423 deletions

3
.gitmodules vendored
View File

@ -37,3 +37,6 @@
[submodule "3rdparty/spdlog"]
path = 3rdparty/spdlog
url = https://github.com/gabime/spdlog.git
[submodule "3rdparty/CLI11"]
path = 3rdparty/CLI11
url = https://github.com/CLIUtils/CLI11.git

1
3rdparty/CLI11 vendored Submodule

@ -0,0 +1 @@
Subproject commit 4160d259d961cd393fd8d67590a8c7d210207348

View File

@ -34,6 +34,11 @@ Build an x86 overlay
Bundle Qt's translations as well
(Default: ${static})
### bundled-cli11
Use the bundled CLI11 version instead of looking for one on the system
(Default: ON)
### bundled-gsl
Use the bundled GSL version instead of looking for one on the system

View File

@ -55,6 +55,7 @@ licenses = [
["licensePCRE", "3rdPartyLicenses/pcre_license.txt", "PCRE", "http://www.pcre.org/", "USE_BUILDENV"],
["licenseQt", "3rdPartyLicenses/qt_license.txt", "Qt", "https://www.qt.io/", "USE_BUILDENV"],
["licenseSQLite3", "3rdPartyLicenses/sqlite3_license.txt", "SQLite3", "http://sqlite.org", "USE_BUILDENV"],
["licenseCLI11", "3rdparty/CLI11/LICENSE", "CLI11", "https://github.com/CLIUtils/CLI11", "USE_BUILDENV"],
["licenseXar", "3rdPartyLicenses/xar_license.txt", "XAR", "https://opensource.apple.com/source/xar/", "USE_BUILDENV"], # macOS only
["licenseAvahi", "3rdPartyLicenses/avahi_license.txt", "Avahi", "https://www.avahi.org/", "USE_BUILDENV"], # Linux only
["licenseAppImageRuntime", "3rdPartyLicenses/appimage_runtime_license.txt", "AppImage Runtime", "http://www.appimage.org", "USE_BUILDENV"], # Linux only

View File

@ -17,6 +17,7 @@ option(tracy "Enable the tracy profiler." OFF)
option(bundled-gsl "Use the bundled GSL version instead of looking for one on the system" ON)
option(bundled-json "Build the included version of nlohmann_json instead of looking for one on the system" ON)
option(bundled-spdlog "Use the bundled spdlog version instead of looking for one on the system" ON)
option(bundled-cli11 "Use the bundled CLI11 version instead of looking for one on the system" ON)
find_pkg(Qt6
VERSION 6.2
@ -251,6 +252,12 @@ else()
endif()
target_link_libraries(shared PUBLIC spdlog::spdlog)
if (bundled-cli11)
add_subdirectory("${3RDPARTY_DIR}/CLI11" "${CMAKE_CURRENT_BINARY_DIR}/CLI11" EXCLUDE_FROM_ALL)
else()
find_pkg("CLI11" 2 REQUIRED)
endif()
if(client)
add_subdirectory(mumble)

View File

@ -392,6 +392,7 @@ target_sources(mumble PRIVATE "ApplicationPalette.h")
target_include_directories(mumble PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
target_link_libraries(mumble PRIVATE mumble_client_object_lib shared spdlog::spdlog)
target_link_libraries(mumble PRIVATE CLI11::CLI11)
target_compile_definitions(mumble_client_object_lib
PUBLIC

View File

@ -53,6 +53,8 @@
#include "widgets/TrayIcon.h"
#include <CLI/CLI.hpp>
#include <QLocale>
#include <QScreen>
#include <QtCore/QProcess>
@ -62,7 +64,9 @@
#include <algorithm>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <spdlog/sinks/dist_sink.h>
#include <spdlog/sinks/qt_sinks.h>
@ -207,6 +211,165 @@ extern int os_early_init();
extern HWND mumble_mw_hwnd;
#endif // Q_OS_WIN
struct CLIOptions {
bool allowMultiple = false;
bool suppressIdentity = false;
bool rpcMode = false;
bool startHiddenInTray = false;
bool printTranslationDirs = false;
bool quit = false;
bool showLicense = false;
bool showAuthors = false;
bool showThirdPartyLicenses = false;
bool dumpInputStreams = false;
bool printEchoCancelQueue = false;
bool skipSettingsBackupPrompt = false;
std::optional< std::string > configFile;
std::optional< std::string > jackClientName;
std::optional< std::string > windowTitleExt;
std::optional< std::string > hyperlink;
std::optional< std::string > translationDir;
std::optional< std::string > locale;
std::optional< std::string > defaultCertDir;
std::optional< std::string > rpcCommand;
static constexpr const char *CLI_GENERAL_SECTION = "General";
static constexpr const char *CLI_LANGUAGE_SECTION = "Language";
static constexpr const char *CLI_ABOUT_SECTION = "About";
static constexpr const char *CLI_DEBUG_SECTION = "Debug";
static constexpr const char *CLI_REMOTE_SECTION = "Remote Control";
static const std::set< std::string > knownRpcCommands;
};
const std::set< std::string > CLIOptions::knownRpcCommands = {
"mute", "unmute", "togglemute", "deaf", "undeaf", "toggledeaf", "starttalking", "stoptalking",
};
CLIOptions parseCLI(int argc, char **argv) {
CLIOptions options;
CLI::App app("Mumble Client");
app.set_version_flag("--version", "Mumble version " + Version::getRelease().toStdString());
app.add_flag("--license", options.showLicense, "Show the Mumble license.")->group(CLIOptions::CLI_ABOUT_SECTION);
app.add_flag("--authors", options.showAuthors, "Show the Mumble authors.")->group(CLIOptions::CLI_ABOUT_SECTION);
app.add_flag("--third-party-licenses", options.showThirdPartyLicenses,
"Show licenses for third-party software used by Mumble.")
->group(CLIOptions::CLI_ABOUT_SECTION);
app.add_option("-c,--config", options.configFile,
"Specify an alternative configuration file. "
"If you use this to run multiple instances of Mumble at once, "
"make sure to use an alternative 'database' value in the config.")
->option_text("<config>")
->check(CLI::ExistingFile)
->group(CLIOptions::CLI_GENERAL_SECTION);
app.add_option("--default-certificate-dir", options.defaultCertDir,
"Specify an alternative default certificate path. "
"This path is only used if there is no certificate loaded "
"from the settings.")
->option_text("<dir>")
->check(CLI::ExistingDirectory)
->group(CLIOptions::CLI_GENERAL_SECTION);
app.add_flag("-m,--multiple", options.allowMultiple, "Allow multiple instances of the client to be started.")
->group(CLIOptions::CLI_GENERAL_SECTION);
app.add_flag("--no-identity", options.suppressIdentity, "Suppress loading of identity files (i.e., certificates).")
->group(CLIOptions::CLI_GENERAL_SECTION);
app.add_option("--jack-name", options.jackClientName, "Set custom Jack client name.")
->option_text("<name>")
->group(CLIOptions::CLI_GENERAL_SECTION);
app.add_option("--window-title-ext", options.windowTitleExt, "Set a custom window title extension.")
->option_text("<extension>")
->group(CLIOptions::CLI_GENERAL_SECTION);
app.add_flag("--skip-settings-backup-prompt", options.skipSettingsBackupPrompt,
"Don't show the settings recovery dialog on startup after a crash.")
->group(CLIOptions::CLI_GENERAL_SECTION);
app.add_flag("--hidden", options.startHiddenInTray, "Start Mumble hidden in the system tray.")
->group(CLIOptions::CLI_GENERAL_SECTION);
app.add_option("--locale", options.locale,
"Overwrite the locale in Mumble's settings with a "
"locale that corresponds to the given locale string. "
"If the format is invalid, Mumble will error. "
"Otherwise the locale will be permanently saved to "
"Mumble's settings.")
->option_text("<locale>")
->group(CLIOptions::CLI_LANGUAGE_SECTION);
app.add_option("--translation-dir", options.translationDir,
"Specifies an additional translation directory <dir> "
"in which Mumble will search for translation files that "
"overwrite the bundled ones. "
"Directories added this way have higher priority than "
"the default locations used otherwise.")
->option_text("<dir>")
->check(CLI::ExistingDirectory)
->group(CLIOptions::CLI_LANGUAGE_SECTION);
app.add_flag("--print-translation-dirs", options.printTranslationDirs,
"Print out the paths in which Mumble will search for "
"translation files that overwrite the bundled ones. "
"(Useful for translators testing their translations)")
->group(CLIOptions::CLI_LANGUAGE_SECTION);
app.add_flag("--dump-input-streams", options.dumpInputStreams,
"Dump PCM streams at various parts of the input chain "
" - raw microphone input "
" - speaker readback for echo cancelling "
" - processed microphone input")
->group(CLIOptions::CLI_DEBUG_SECTION);
app.add_flag("--print-echocancel-queue", options.printEchoCancelQueue,
"Print on stdout the echo cancellation queue state")
->group(CLIOptions::CLI_DEBUG_SECTION);
app.add_option(
"hyperlink", options.hyperlink,
"<url> specifies a URL to connect to after startup instead of showing "
"the connection window, and has the following form:\n"
"mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/"
"<subchannel>...]][?version=<x.y.z>]\n"
"\n"
"<plugin_list> is a list of plugin files that shall be installed"
"\n"
"The version query parameter has to be set in order to invoke the\n"
"correct client version. It currently defaults to 1.2.0.\n"
"\n"
"This allows setup of program associations with mumble to directly connect to servers or run with plugins.")
->option_text("<url> | <plugin_list>")
->group(CLIOptions::CLI_GENERAL_SECTION);
CLI::App *rpc = app.add_subcommand("rpc", "Remote control a running instance of Mumble");
rpc->add_option("action", options.rpcCommand, "Action to perform")
->required()
->check(CLI::IsMember(CLIOptions::knownRpcCommands, CLI::ignore_case));
try {
app.parse(argc, argv);
} catch (const CLI::ParseError &e) {
std::stringstream info_stream, error_stream;
app.exit(e, info_stream, error_stream);
if (e.get_exit_code() != static_cast< int >(CLI::ExitCodes::Success)) {
qWarning("%s", error_stream.str().c_str());
} else {
qInfo("%s", info_stream.str().c_str());
}
options.quit = true;
}
return options;
}
int main(int argc, char **argv) {
int res = 0;
@ -239,25 +402,24 @@ int main(int argc, char **argv) {
#endif
MumbleSSL::initialize();
CLIOptions options = parseCLI(argc, argv);
if (options.quit) {
return 0;
}
// This argument has to be parsed first, since it's value is needed to create the global struct,
// which other switches are modifying. If it is parsed first, the order of the arguments does not matter.
QString settingsFile;
QStringList args = a.arguments();
const auto index = std::max(args.lastIndexOf(QLatin1String("-c")), args.lastIndexOf(QLatin1String("--config")));
if (index >= 0) {
if (index + 1 < args.count()) {
QFile inifile(args.at(index + 1));
if (inifile.exists() && inifile.permissions().testFlag(QFile::WriteUser)) {
Global::g_global_struct = new Global(args.at(index + 1));
settingsFile = args.at(index + 1);
} else {
printf("%s", qPrintable(MainWindow::tr("Configuration file %1 does not exist or is not writable.\n")
.arg(args.at(index + 1))));
return 1;
}
if (options.configFile) {
QFile inifile(QString::fromStdString(*options.configFile));
if (inifile.exists() && inifile.permissions().testFlag(QFile::WriteUser)) {
Global::g_global_struct = new Global(inifile.fileName());
settingsFile = inifile.fileName();
} else {
qCritical("Missing argument for --config!");
printf("%s", qPrintable(MainWindow::tr("Configuration file %1 does not exist or is not writable.\n")
.arg(inifile.fileName())));
return 1;
}
} else {
@ -270,256 +432,88 @@ int main(int argc, char **argv) {
os_init();
bool bAllowMultiple = false;
bool suppressIdentity = false;
bool customJackClientName = false;
bool bRpcMode = false;
bool printTranslationDirs = false;
bool startHiddenInTray = false;
bool skipSettingsBackupPrompt = false;
QString rpcCommand;
QUrl url;
QDir qdCert(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
QStringList extraTranslationDirs;
QString localeOverwrite;
QStringList pluginsToBeInstalled;
if (a.arguments().count() > 1) {
for (int i = 1; i < args.count(); ++i) {
if (args.at(i) == QLatin1String("-h") || args.at(i) == QLatin1String("--help")
#if defined(Q_OS_WIN)
|| args.at(i) == QLatin1String("/?")
#endif
) {
QString helpMessage =
MainWindow::tr("Usage: mumble [options] [<url> | <plugin_list>]\n"
"\n"
"<url> specifies a URL to connect to after startup instead of showing\n"
"the connection window, and has the following form:\n"
"mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/"
"<subchannel>...]][?version=<x.y.z>]\n"
"\n"
"<plugin_list> is a list of plugin files that shall be installed"
"\n"
"The version query parameter has to be set in order to invoke the\n"
"correct client version. It currently defaults to 1.2.0.\n"
"\n"
"Valid options are:\n"
" -h, --help Show this help text and exit.\n"
" --version Print version information and exit\n"
" -m, --multiple\n"
" Allow multiple instances of the client to be started.\n"
" -c, --config\n"
" Specify an alternative configuration file.\n"
" If you use this to run multiple instances of Mumble at once,\n"
" make sure to set an alternative 'database' value in the config.\n"
" --default-certificate-dir <dir>\n"
" Specify an alternative default certificate path.\n"
" This path is only used if there is no certificate loaded\n"
" from the settings.\n"
" -n, --noidentity\n"
" Suppress loading of identity files (i.e., certificates.)\n"
" -jn, --jackname <arg>\n"
" Set custom Jack client name.\n"
" --license\n"
" Show the Mumble license.\n"
" --authors\n"
" Show the Mumble authors.\n"
" --third-party-licenses\n"
" Show licenses for third-party software used by Mumble.\n"
" --window-title-ext <arg>\n"
" Sets a custom window title extension.\n"
" --dump-input-streams\n"
" Dump PCM streams at various parts of the input chain\n"
" (useful for debugging purposes)\n"
" - raw microphone input\n"
" - speaker readback for echo cancelling\n"
" - processed microphone input\n"
" --print-echocancel-queue\n"
" Print on stdout the echo cancellation queue state\n"
" (useful for debugging purposes)\n"
" --translation-dir <dir>\n"
" Specifies an additional translation directory <dir>\n"
" in which Mumble will search for translation files that\n"
" overwrite the bundled ones\n"
" Directories added this way have higher priority than\n"
" the default locations used otherwise\n"
" --print-translation-dirs\n"
" Print out the paths in which Mumble will search for\n"
" translation files that overwrite the bundled ones.\n"
" (Useful for translators testing their translations)\n"
" --locale <locale>\n"
" Overwrite the locale in Mumble's settings with a\n"
" locale that corresponds to the given locale string.\n"
" If the format is invalid, Mumble will error.\n"
" Otherwise the locale will be permanently saved to\n"
" Mumble's settings.\n"
" --hidden\n"
" Start Mumble hidden in the system tray.\n"
" --skip-settings-backup-prompt\n"
" Don't show the settings recovery dialog on startup after a crash."
"\n");
QString rpcHelpBanner = MainWindow::tr("Remote controlling Mumble:\n"
"\n");
QString rpcHelpMessage =
MainWindow::tr("Usage: mumble rpc <action> [options]\n"
"\n"
"It is possible to remote control a running instance of Mumble by using\n"
"the 'mumble rpc' command.\n"
"\n"
"Valid actions are:\n"
" mute\n"
" Mute self\n"
" unmute\n"
" Unmute self\n"
" togglemute\n"
" Toggle self-mute status\n"
" deaf\n"
" Deafen self\n"
" undeaf\n"
" Undeafen self\n"
" toggledeaf\n"
" Toggle self-deafen status\n"
" starttalking\n"
" Start talking\n"
" stoptalking\n"
" Stop talking\n"
"\n");
QString helpOutput = helpMessage + rpcHelpBanner + rpcHelpMessage;
if (bRpcMode) {
helpOutput = rpcHelpMessage;
}
#if defined(Q_OS_WIN)
QMessageBox::information(nullptr, MainWindow::tr("Invocation"), helpOutput);
#else
printf("%s", qPrintable(helpOutput));
#endif
return 1;
} else if (args.at(i) == QLatin1String("-m") || args.at(i) == QLatin1String("--multiple")) {
bAllowMultiple = true;
} else if (args.at(i) == QLatin1String("-n") || args.at(i) == QLatin1String("--noidentity")) {
suppressIdentity = true;
Global::get().s.bSuppressIdentity = true;
} else if (args.at(i) == QLatin1String("-jn") || args.at(i) == QLatin1String("--jackname")) {
if (i + 1 < args.count()) {
Global::get().s.qsJackClientName = QString(args.at(i + 1));
customJackClientName = true;
++i;
} else {
qCritical("Missing argument for --jackname!");
return 1;
}
} else if (args.at(i) == QLatin1String("--window-title-ext")) {
if (i + 1 < args.count()) {
Global::get().windowTitlePostfix = QString(args.at(i + 1));
++i;
} else {
qCritical("Missing argument for --window-title-ext!");
return 1;
}
} else if (args.at(i) == QLatin1String("-license") || args.at(i) == QLatin1String("--license")) {
printf("%s\n", qPrintable(License::license()));
return 0;
} else if (args.at(i) == QLatin1String("-authors") || args.at(i) == QLatin1String("--authors")) {
printf("%s\n",
"For a list of authors, please see https://github.com/mumble-voip/mumble/graphs/contributors");
return 0;
} else if (args.at(i) == QLatin1String("-third-party-licenses")
|| args.at(i) == QLatin1String("--third-party-licenses")) {
printf("%s", qPrintable(License::printableThirdPartyLicenseInfo()));
return 0;
} else if (args.at(i) == QLatin1String("rpc")) {
bRpcMode = true;
if (args.count() - 1 > i) {
rpcCommand = QString(args.at(i + 1));
} else {
QString rpcError = MainWindow::tr("Error: No RPC command specified");
#if defined(Q_OS_WIN)
QMessageBox::information(nullptr, MainWindow::tr("RPC"), rpcError);
#else
printf("%s\n", qPrintable(rpcError));
#endif
return 1;
}
} else if (args.at(i) == QLatin1String("--dump-input-streams")) {
Global::get().bDebugDumpInput = true;
} else if (args.at(i) == QLatin1String("--print-echocancel-queue")) {
Global::get().bDebugPrintQueue = true;
} else if (args.at(i) == QLatin1String("-c") || args.at(i) == QLatin1String("--config")) {
// We already parsed these arguments above, so just skip over them here
++i;
} else if (args.at(i) == QLatin1String("--default-certificate-dir")) {
if (i + 1 < args.count()) {
qdCert = QDir(args.at(i + 1));
// I suppose we should really be checking whether the directory is writable here too,
// but there are some subtleties with doing that:
// (doc.qt.io/qt-5/qfile.html#platform-specific-issues)
// so we can just let things fail down below when this directory is used.
if (!qdCert.exists()) {
printf("%s", qPrintable(MainWindow::tr("Directory %1 does not exist.\n").arg(args.at(i + 1))));
return 1;
}
++i;
} else {
qCritical("Missing argument for --default-certificate-dir!");
return 1;
}
} else if (args.at(i) == "--print-translation-dirs") {
printTranslationDirs = true;
} else if (args.at(i) == "--translation-dir") {
if (i + 1 < args.count()) {
extraTranslationDirs.append(args.at(i + 1));
i++;
} else {
qCritical("Missing argument for --translation-dir!");
return 1;
}
} else if (args.at(i) == "--locale") {
if (i + 1 < args.count()) {
localeOverwrite = args.at(i + 1);
i++;
} else {
qCritical("Missing argument for --locale!");
return 1;
}
} else if (args.at(i) == "--hidden") {
if (options.suppressIdentity) {
Global::get().s.bSuppressIdentity = true;
}
if (options.jackClientName) {
Global::get().s.qsJackClientName = QString::fromStdString(*options.jackClientName);
}
if (options.windowTitleExt) {
Global::get().windowTitlePostfix = QString::fromStdString(*options.windowTitleExt);
}
if (options.showLicense) {
printf("%s\n", qPrintable(License::license()));
return 0;
}
if (options.showAuthors) {
printf("%s\n", "For a list of authors, please see https://github.com/mumble-voip/mumble/graphs/contributors");
return 0;
}
if (options.showThirdPartyLicenses) {
printf("%s", qPrintable(License::printableThirdPartyLicenseInfo()));
return 0;
}
if (options.rpcCommand) {
options.rpcMode = true;
}
if (options.dumpInputStreams) {
Global::get().bDebugDumpInput = true;
}
if (options.printEchoCancelQueue) {
Global::get().bDebugPrintQueue = true;
}
if (options.defaultCertDir) {
qdCert = QDir(QString::fromStdString(*options.defaultCertDir));
// I suppose we should really be checking whether the directory is writable here too,
// but there are some subtleties with doing that:
// (doc.qt.io/qt-5/qfile.html#platform-specific-issues)
// so we can just let things fail down below when this directory is used.
if (!qdCert.exists()) { // probably not reached since cli11 checks for existence
printf("%s", qPrintable(MainWindow::tr("Directory %1 does not exist.\n").arg(qdCert.absolutePath())));
return 1;
}
}
if (options.translationDir) {
extraTranslationDirs.append(QString::fromStdString(*options.translationDir));
}
if (options.locale) {
localeOverwrite = QString::fromStdString(*options.locale);
}
if (options.startHiddenInTray) {
#ifndef Q_OS_MAC
startHiddenInTray = true;
qInfo("Starting hidden in system tray");
qInfo("Starting hidden in system tray");
#else
// When Qt addresses hide() on macOS to use native hiding, this can be fixed.
qWarning("Can not start Mumble hidden in system tray on macOS");
// When Qt addresses hide() on macOS to use native hiding, this can be fixed.
qWarning("Can not start Mumble hidden in system tray on macOS");
#endif
} else if (args.at(i) == QLatin1String("--skip-settings-backup-prompt")) {
skipSettingsBackupPrompt = true;
} else if (args.at(i) == "--version") {
// Print version and exit (print to regular std::cout to avoid adding any useless meta-information from
// using e.g. qWarning
std::cout << "Mumble version " << Version::getRelease().toStdString() << std::endl;
return 0;
} else {
if (PluginInstaller::canBePluginFile(QFileInfo(args.at(i)))) {
pluginsToBeInstalled << args.at(i);
}
if (options.hyperlink) {
if (PluginInstaller::canBePluginFile(QFileInfo(QString::fromStdString(*options.hyperlink)))) {
pluginsToBeInstalled << QString::fromStdString(*options.hyperlink);
} else {
if (!options.rpcMode) {
QString qHyperlink = QString::fromStdString(*options.hyperlink);
QUrl u = QUrl::fromEncoded(qHyperlink.toUtf8());
if (u.isValid() && (u.scheme() == QLatin1String("mumble"))) {
url = u;
} else {
if (!bRpcMode) {
QUrl u = QUrl::fromEncoded(args.at(i).toUtf8());
if (u.isValid() && (u.scheme() == QLatin1String("mumble"))) {
url = u;
} else {
QFile f(args.at(i));
if (f.exists()) {
url = QUrl::fromLocalFile(f.fileName());
}
}
QFile f(qHyperlink);
if (f.exists()) {
url = QUrl::fromLocalFile(f.fileName());
}
}
}
}
}
if (printTranslationDirs) {
if (options.printTranslationDirs) {
QString infoString = QObject::tr("The directories in which Mumble searches for extra translation files are:\n");
int counter = 1;
@ -571,9 +565,10 @@ int main(int argc, char **argv) {
# endif
#endif
if (bRpcMode) {
if (options.rpcMode) {
bool sent = false;
QMap< QString, QVariant > param;
QString rpcCommand = QString::fromStdString(*options.rpcCommand);
param.insert(rpcCommand, rpcCommand);
sent = SocketRPC::send(QLatin1String("Mumble"), QLatin1String("self"), param);
if (sent) {
@ -583,7 +578,7 @@ int main(int argc, char **argv) {
}
}
if (!bAllowMultiple) {
if (!options.allowMultiple) {
if (url.isValid()) {
#ifndef USE_DBUS
QMap< QString, QVariant > param;
@ -628,7 +623,7 @@ int main(int argc, char **argv) {
// If another Mumble instance attempts to open the file, it will fail,
// and that instance will know to terminate itself.
UserLockFile userLockFile(Global::get().qdBasePath.filePath(QLatin1String("mumble.lock")));
if (!bAllowMultiple) {
if (!options.allowMultiple) {
if (!userLockFile.acquire()) {
qWarning("Another process has already acquired the lock file at '%s'. Terminating...",
qPrintable(userLockFile.path()));
@ -639,9 +634,9 @@ int main(int argc, char **argv) {
// Load preferences
if (settingsFile.isEmpty()) {
Global::get().s.load(skipSettingsBackupPrompt);
Global::get().s.load();
} else {
Global::get().s.load(settingsFile, skipSettingsBackupPrompt);
Global::get().s.load(settingsFile);
}
if (!Global::get().migratedDBPath.isEmpty()) {
// We have migrated the DB to a new location. Make sure that the settings hold the correct (new) path and that
@ -764,7 +759,7 @@ int main(int argc, char **argv) {
// Main Window
Global::get().mw = new MainWindow(nullptr);
if (!startHiddenInTray) {
if (!options.startHiddenInTray) {
Global::get().mw->showRaiseWindow();
}
@ -881,7 +876,7 @@ int main(int argc, char **argv) {
OpenURLEvent *oue = new OpenURLEvent(a.quLaunchURL);
qApp->postEvent(Global::get().mw, oue);
#endif
} else if (!startHiddenInTray || Global::get().s.bAutoConnect) {
} else if (!options.startHiddenInTray || Global::get().s.bAutoConnect) {
Global::get().mw->on_qaServerConnect_triggered(true);
}
@ -997,12 +992,12 @@ int main(int argc, char **argv) {
if (res == MUMBLE_EXIT_CODE_RESTART) {
QStringList arguments;
if (bAllowMultiple)
if (options.allowMultiple)
arguments << QLatin1String("--multiple");
if (suppressIdentity)
arguments << QLatin1String("--noidentity");
if (customJackClientName)
arguments << QLatin1String("--jackname ") + Global::get().s.qsJackClientName;
if (options.suppressIdentity)
arguments << QLatin1String("--no-identity");
if (options.jackClientName)
arguments << QLatin1String("--jack-name ") + Global::get().s.qsJackClientName;
if (!url.isEmpty())
arguments << url.toString();

View File

@ -58,7 +58,7 @@ else()
add_executable(mumble-server "main.cpp")
endif()
target_link_libraries(mumble-server mumble_server_object_lib)
target_link_libraries(mumble-server mumble_server_object_lib CLI11::CLI11)
set_property(TARGET mumble-server PROPERTY INTERPROCEDURAL_OPTIMIZATION ${lto})
set_property(TARGET mumble_server_object_lib PROPERTY INTERPROCEDURAL_OPTIMIZATION ${lto})
@ -139,8 +139,8 @@ if(WIN32)
find_pkg(Qt6 COMPONENTS Widgets REQUIRED)
target_link_libraries(mumble_server_object_lib
PUBLIC
target_link_libraries(mumble_server_object_lib
PUBLIC
Qt6::Widgets
dbghelp.lib
shlwapi.lib

View File

@ -38,6 +38,11 @@
# include <sys/syslog.h>
#endif
#include <optional>
#include <tuple>
#include <CLI/CLI.hpp>
extern QFile *qfLog;
static bool bVerbose = false;
@ -187,6 +192,126 @@ void cleanup(int signum) {
exit(signum);
}
struct CLIOptions {
bool quit = false;
std::optional< std::string > iniFile;
std::optional< std::string > dbDumpPath;
std::optional< std::string > dbImportPath;
std::tuple< std::string, std::optional< unsigned int > > supwSrv;
std::optional< unsigned int > disableSuSrv;
bool verboseLogging = false;
bool cliDetach = detach;
bool wipeSsl = false;
bool wipeLogs = false;
bool logGroups = false;
bool logAcls = false;
bool printAuthors = false;
bool printLicense = false;
bool printThirdPartyLicenses = false;
#ifdef Q_OS_UNIX
bool limits = false;
std::optional< unsigned int > readSupwSrv;
#endif
static constexpr const char *const CLI_ABOUT_SECTION = "About";
static constexpr const char *const CLI_LOGGING_SECTION = "Logging";
static constexpr const char *const CLI_ADMINISTRATION_SECTION = "Administration";
static constexpr const char *const CLI_CONFIGURATION_SECTION = "Configuration";
static constexpr const char *const CLI_TESTING_SECTION = "Testing";
};
CLIOptions parseCLI(int argc, char **argv) {
CLIOptions options;
CLI::App app;
app.set_version_flag("--version", Version::getRelease().toStdString());
app.add_option("-i,--ini", options.iniFile, "Specify ini file to use.")
->option_text("<inifile>")
->check(CLI::ExistingFile)
->group(CLIOptions::CLI_CONFIGURATION_SECTION);
app.add_option("--set-su-pw", options.supwSrv, "Set password for 'SuperUser' account on server srv.")
->option_text("<pw> [srv]")
->type_size(1, 2)
->group(CLIOptions::CLI_ADMINISTRATION_SECTION);
#ifdef Q_OS_UNIX
app.add_option("--read-su-pw", options.readSupwSrv, "Reads password for server srv from standard input.")
->option_text("[srv]")
->default_str("0")
->expected(0, 1)
->group(CLIOptions::CLI_ADMINISTRATION_SECTION);
app.add_flag("--limits", options.limits,
"Tests and shows how many file descriptors and threads can be created. "
"The purpose of this option is to test how many clients Mumble server can handle. "
"Mumble server will exit after this test.")
->group(CLIOptions::CLI_TESTING_SECTION);
#endif
app.add_option_no_stream("--disable-su", options.disableSuSrv,
"Disable password for 'SuperUser' account on server srv.")
->option_text("[srv]")
->expected(0, 1)
->group(CLIOptions::CLI_ADMINISTRATION_SECTION);
app.add_flag("--wipe-ssl", options.wipeSsl, "Remove SSL certificates from database.")
->group(CLIOptions::CLI_ADMINISTRATION_SECTION);
app.add_option("--db-json-dump", options.dbDumpPath,
"Requests a JSON dump of the database to be written to the given file")
->option_text("<file>")
->group(CLIOptions::CLI_ADMINISTRATION_SECTION);
app.add_option("--db-json-import", options.dbImportPath,
"Reads in the provide JSON file and imports its contents into the database")
->option_text("<file>")
->group(CLIOptions::CLI_ADMINISTRATION_SECTION);
app.add_flag("-v,--verbose", options.verboseLogging, "Use verbose logging (include debug-logs).")
->group(CLIOptions::CLI_LOGGING_SECTION);
app.add_flag("--detach,!--no-detach,--background,!--foreground", options.cliDetach,
"Whether to run in detached/background mode. In this mode, the program will detach and run as an "
"independent process in the background. Furthermore, logs will be written to the database instead of "
"to the console.")
->group(CLIOptions::CLI_LOGGING_SECTION);
app.add_flag("--wipe-logs", options.wipeLogs, "Remove all log entries from database.")
->group(CLIOptions::CLI_LOGGING_SECTION);
app.add_flag("--log-groups", options.logGroups, "Turns on logging for group changes for all servers.")
->group(CLIOptions::CLI_LOGGING_SECTION);
app.add_flag("--log-acls", options.logAcls, "Turns on logging for ACL changes for all servers.")
->group(CLIOptions::CLI_LOGGING_SECTION);
app.add_flag("--authors", options.printAuthors, "Show Mumble server's authors.")
->group(CLIOptions::CLI_ABOUT_SECTION);
app.add_flag("--license", options.printLicense, "Show Mumble server's license.")
->group(CLIOptions::CLI_ABOUT_SECTION);
app.add_flag("--third-party-licenses", options.printThirdPartyLicenses,
"Show licenses for third-party software used by Mumble server.")
->group(CLIOptions::CLI_ABOUT_SECTION);
app.footer("If no inifile is provided, Mumble server will search for one in default locations.");
try {
(app).parse(argc, argv);
} catch (const CLI::ParseError &e) {
std::stringstream info_stream, error_stream;
app.exit(e, info_stream, error_stream);
if (e.get_exit_code() != static_cast< int >(CLI::ExitCodes::Success)) {
qWarning("%s", error_stream.str().c_str());
} else {
qInfo("%s", info_stream.str().c_str());
}
options.quit = true;
}
return options;
}
int main(int argc, char **argv) {
try {
// Check for SSE and MMX, but only in the windows binaries
@ -236,180 +361,87 @@ int main(int argc, char **argv) {
MumbleSSL::initialize();
QString inifile;
QString supw;
QString dbDumpPath;
QString dbImportPath;
bool disableSu = false;
bool wipeSsl = false;
bool wipeLogs = false;
unsigned int sunum = 0;
#ifdef Q_OS_UNIX
bool readPw = false;
#endif
bool logGroups = false;
bool logACL = false;
qInstallMessageHandler(murmurMessageOutputWithContext);
#ifdef Q_OS_WIN
Tray tray(nullptr, &le);
#endif
CLIOptions cli_options = parseCLI(argc, argv);
if (cli_options.quit)
return 0;
QStringList args = a.arguments();
for (int i = 1; i < args.size(); i++) {
bool bLast = false;
QString arg = args.at(i).toLower();
if ((arg == "-supw")) {
detach = false;
if (i + 1 < args.size()) {
i++;
supw = args.at(i);
if (i + 1 < args.size()) {
i++;
sunum = args.at(i).toUInt();
}
bLast = true;
} else {
#ifdef Q_OS_UNIX
qFatal("-supw expects the password on the command line - maybe you meant -readsupw?");
#else
qFatal("-supw expects the password on the command line");
#endif
}
#ifdef Q_OS_UNIX
} else if ((arg == "-readsupw")) {
// Note that it is essential to set detach = false here. If this is ever to be changed, the code part
// handling the readPw = true part has to be moved up so that it is executed before fork is called on
// Unix systems.
detach = false;
readPw = true;
if (i + 1 < args.size()) {
i++;
sunum = args.at(i).toUInt();
}
bLast = true;
#endif
} else if ((arg == "-disablesu")) {
detach = false;
disableSu = true;
if (i + 1 < args.size()) {
i++;
sunum = args.at(i).toUInt();
}
bLast = true;
} else if ((arg == "-ini") && (i + 1 < args.size())) {
i++;
inifile = args.at(i);
} else if ((arg == "-wipessl")) {
wipeSsl = true;
} else if ((arg == "-wipelogs")) {
wipeLogs = true;
} else if ((arg == "-fg")) {
detach = false;
} else if ((arg == "-v")) {
bVerbose = true;
} else if ((arg == "-version") || (arg == "--version")) {
// Print version and exit (print to regular std::cout to avoid adding any useless meta-information from
// using e.g. qWarning
std::cout << "Mumble server version " << Version::getRelease().toStdString() << std::endl;
return 0;
} else if (args.at(i) == QLatin1String("-license") || args.at(i) == QLatin1String("--license")) {
if (cli_options.printLicense) {
#ifdef Q_OS_WIN
AboutDialog ad(nullptr, AboutDialogOptionsShowLicense);
ad.exec();
return 0;
AboutDialog ad(nullptr, AboutDialogOptionsShowLicense);
ad.exec();
return 0;
#else
qInfo("%s\n", qPrintable(License::license()));
return 0;
qInfo("%s\n", qPrintable(License::license()));
return 0;
#endif
} else if (args.at(i) == QLatin1String("-authors") || args.at(i) == QLatin1String("--authors")) {
} else if (cli_options.printAuthors) {
#ifdef Q_OS_WIN
AboutDialog ad(nullptr, AboutDialogOptionsShowAuthors);
ad.exec();
return 0;
AboutDialog ad(nullptr, AboutDialogOptionsShowAuthors);
ad.exec();
return 0;
#else
qInfo("%s\n",
"For a list of authors, please see https://github.com/mumble-voip/mumble/graphs/contributors");
return 0;
qInfo("%s\n",
"For a list of authors, please see https://github.com/mumble-voip/mumble/graphs/contributors");
return 0;
#endif
} else if (args.at(i) == QLatin1String("-third-party-licenses")
|| args.at(i) == QLatin1String("--third-party-licenses")) {
} else if (cli_options.printThirdPartyLicenses) {
#ifdef Q_OS_WIN
AboutDialog ad(nullptr, AboutDialogOptionsShowThirdPartyLicenses);
ad.exec();
return 0;
AboutDialog ad(nullptr, AboutDialogOptionsShowThirdPartyLicenses);
ad.exec();
return 0;
#else
qInfo("%s", qPrintable(License::printableThirdPartyLicenseInfo()));
return 0;
qInfo("%s", qPrintable(License::printableThirdPartyLicenseInfo()));
return 0;
#endif
} else if (arg == "--db-json-dump") {
++i;
dbDumpPath = args.at(i);
} else if (arg == "--db-json-import") {
++i;
dbImportPath = args.at(i);
} else if ((arg == "-h") || (arg == "-help") || (arg == "--help")) {
detach = false;
qInfo(
"Usage: %s [-ini <inifile>] [-supw <password>]\n"
" --version Print version information and exit\n"
" -ini <inifile> Specify ini file to use.\n"
" -supw <pw> [srv] Set password for 'SuperUser' account on server srv.\n"
}
detach = cli_options.cliDetach;
QString inifile = QString::fromStdString(cli_options.iniFile.value_or(""));
QString supw;
bool disableSu = false;
bool wipeSsl = cli_options.wipeSsl;
bool wipeLogs = cli_options.wipeLogs;
unsigned int sunum = 0;
#ifdef Q_OS_UNIX
" -readsupw [srv] Reads password for server srv from standard input.\n"
bool readPw = false;
#endif
" -disablesu [srv] Disable password for 'SuperUser' account on server srv.\n"
bool logGroups = cli_options.logGroups;
bool logACL = cli_options.logAcls;
bVerbose = cli_options.verboseLogging;
if (cli_options.disableSuSrv) {
detach = false;
disableSu = true;
sunum = *cli_options.disableSuSrv;
}
if (!std::get< 0 >(cli_options.supwSrv).empty()) {
supw = QString::fromStdString(std::get< 0 >(cli_options.supwSrv));
sunum = std::get< 1 >(cli_options.supwSrv).value_or< unsigned int >(0);
#ifdef Q_OS_UNIX
" -limits Tests and shows how many file descriptors and threads can be created.\n"
" The purpose of this option is to test how many clients Murmur can "
"handle.\n"
" Murmur will exit after this test.\n"
} else if (cli_options.readSupwSrv) {
// Note that it is essential to set detach = false here. If this is ever to be changed, the code part
// handling the readPw = true part has to be moved up so that it is executed before fork is called on Unix
// systems.
detach = false;
readPw = true;
sunum = *cli_options.readSupwSrv;
}
if (cli_options.limits) {
detach = false;
Meta::mp->read(inifile);
unixhandler.setuid();
unixhandler.finalcap();
LimitTest::testLimits(a);
#endif
" -v Use verbose logging (include debug-logs).\n"
#ifdef Q_OS_UNIX
" -fg Don't detach from console.\n"
#else
" -fg Don't write to the log file.\n"
#endif
" -wipessl Remove SSL certificates from database.\n"
" -wipelogs Remove all log entries from database.\n"
" -loggroups Turns on logging for group changes for all servers.\n"
" -logacls Turns on logging for ACL changes for all servers.\n"
" -version Show version information.\n"
" --db-json-dump [file] Requests a JSON dump of the database to be written to the given file\n"
" --db-json-import [file] Reads in the provide JSON file and imports its contents into the "
"database\n"
"\n"
" -license Show Murmur's license.\n"
" -authors Show Murmur's authors.\n"
" -third-party-licenses Show licenses for third-party software used by Murmur.\n"
"\n"
"If no inifile is provided, murmur will search for one in \n"
"default locations.",
qPrintable(args.at(0)));
return 0;
#ifdef Q_OS_UNIX
} else if (arg == "-limits") {
detach = false;
Meta::mp->read(inifile);
unixhandler.setuid();
unixhandler.finalcap();
LimitTest::testLimits(a);
#endif
} else if (arg == "-loggroups") {
logGroups = true;
} else if (arg == "-logacls") {
logACL = true;
} else {
detach = false;
qFatal("Unknown argument %s", qPrintable(args.at(i)));
}
if (bLast && (i + 1 != args.size())) {
detach = false;
qFatal("Password arguments must be last.");
}
}
if (QSslSocket::supportsSsl()) {
@ -424,22 +456,22 @@ int main(int argc, char **argv) {
Meta::mp->read(inifile);
if (!dbDumpPath.isEmpty()) {
if (cli_options.dbDumpPath) {
DBWrapper wrapper(Meta::getConnectionParameter());
std::ofstream file(dbDumpPath.toStdString());
std::ofstream file(*cli_options.dbDumpPath);
file << wrapper.exportDBToJSON().dump(2);
qInfo("Dumped JSON representation of database contents to '%s'", qPrintable(dbDumpPath));
qInfo("Dumped JSON representation of database contents to '%s'", cli_options.dbDumpPath->c_str());
return 0;
}
if (!dbImportPath.isEmpty()) {
qInfo("Importing contents of '%s' into database", qPrintable(dbImportPath));
if (cli_options.dbImportPath) {
qInfo("Importing contents of '%s' into database", cli_options.dbImportPath->c_str());
DBWrapper wrapper(Meta::getConnectionParameter());
std::ifstream file(dbImportPath.toStdString());
std::ifstream file(*cli_options.dbImportPath);
nlohmann::json json;
file >> json;