Improve startup/shutdown behavior; add `--replace` CLI option

* Make functions in `main.cpp` static as they are not used by
  other units
* Delete the `TrayIcon` via an extra parent object that is
  deleted before the `QNetworkAccessManager` is deleted; otherwise
  the destruction of `SyncthingConnection` (which aborts pending
  replies) might access dangling `QNetworkReply` objects
* Improve error handling in `SingleInstance` code
* Allow to replace the current instance via the new `--replace`
  argument; this may be useful when creating an installer/updater
This commit is contained in:
Martchus 2023-04-20 00:33:52 +02:00
parent 0c733837ce
commit c9cd81311d
3 changed files with 106 additions and 55 deletions

View File

@ -22,6 +22,7 @@
#include <c++utilities/application/argumentparser.h>
#include <c++utilities/application/commandlineutils.h>
#include <c++utilities/io/ansiescapecodes.h>
#include <c++utilities/misc/parseerror.h>
#include <qtutilities/misc/dialogutils.h>
@ -52,7 +53,7 @@ Q_IMPORT_PLUGIN(ForkAwesomeIconEnginePlugin)
ENABLE_QT_RESOURCES_OF_STATIC_DEPENDENCIES
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
void handleSystemdServiceError(const QString &context, const QString &name, const QString &message)
static void handleSystemdServiceError(const QString &context, const QString &name, const QString &message)
{
auto *const msgBox = new QMessageBox;
msgBox->setAttribute(Qt::WA_DeleteOnClose);
@ -63,7 +64,9 @@ void handleSystemdServiceError(const QString &context, const QString &name, cons
}
#endif
int initSyncthingTray(bool windowed, bool waitForTray, const Argument &connectionConfigArg)
QObject *parentObject = nullptr;
static int initSyncthingTray(bool windowed, bool waitForTray, const Argument &connectionConfigArg)
{
// get settings
auto &settings = Settings::values();
@ -103,7 +106,7 @@ int initSyncthingTray(bool windowed, bool waitForTray, const Argument &connectio
// show a tray icon for each connection
TrayWidget *widget;
for (const auto *const connectionConfig : connectionConfigurations) {
auto *const trayIcon = new TrayIcon(QString::fromLocal8Bit(connectionConfig), QApplication::instance());
auto *const trayIcon = new TrayIcon(QString::fromLocal8Bit(connectionConfig), parentObject);
trayIcon->show();
widget = &trayIcon->trayMenu().widget();
}
@ -141,7 +144,7 @@ static void trigger(bool tray, bool webUi, bool wizard)
}
}
void shutdownSyncthingTray()
static void shutdownSyncthingTray()
{
Settings::save();
if (const auto &error = Settings::values().error; !error.isEmpty()) {
@ -150,7 +153,7 @@ void shutdownSyncthingTray()
Settings::Launcher::terminate();
}
int runApplication(int argc, const char *const *argv)
static int runApplication(int argc, const char *const *argv)
{
// setup argument parser
SET_APPLICATION_INFO;
@ -174,9 +177,12 @@ int runApplication(int argc, const char *const *argv)
configPathArg.setEnvironmentVariable(PROJECT_VARNAME_UPPER "_CONFIG_DIR");
auto singleInstanceArg = Argument("single-instance", '\0', "does nothing if a tray icon is already shown");
auto newInstanceArg = Argument("new-instance", '\0', "disable the usual single-process behavior");
auto replaceArg = Argument("replace", '\0', "replaces a currently running instance");
auto quitArg = OperationArgument("quit", '\0', "quits the currently running instance");
quitArg.setFlags(Argument::Flags::Deprecated, true); // hide as only used internally for --replace
auto &widgetsGuiArg = qtConfigArgs.qtWidgetsGuiArg();
widgetsGuiArg.addSubArguments({ &windowedArg, &showWebUiArg, &triggerArg, &waitForTrayArg, &connectionArg, &configPathArg, &singleInstanceArg,
&newInstanceArg, &showWizardArg, &assumeFirstLaunchArg, &wipArg });
&newInstanceArg, &replaceArg, &showWizardArg, &assumeFirstLaunchArg, &wipArg });
#ifdef SYNCTHINGTRAY_USE_LIBSYNCTHING
auto cliArg = OperationArgument("cli", 'c', "run Syncthing's CLI");
auto cliHelp = ConfigValueArgument("help", 'h', "show help for Syncthing's CLI");
@ -189,7 +195,9 @@ int runApplication(int argc, const char *const *argv)
#ifdef SYNCTHINGTRAY_USE_LIBSYNCTHING
&cliArg,
#endif
&parser.noColorArg(), &parser.helpArg() });
&parser.noColorArg(), &parser.helpArg(), &quitArg });
// parse arguments
parser.parseArgs(argc, argv);
#ifdef SYNCTHINGTRAY_USE_LIBSYNCTHING
if (cliArg.isPresent()) {
@ -197,8 +205,18 @@ int runApplication(int argc, const char *const *argv)
return static_cast<int>(LibSyncthing::runCli(cliArg.values()));
}
#endif
// quit already running application if quit is present
static auto firstRun = true;
if (quitArg.isPresent() && !firstRun) {
std::cerr << EscapeCodes::Phrases::Info << "Quitting as told by another instance" << EscapeCodes::Phrases::EndFlush;
QCoreApplication::quit();
return EXIT_SUCCESS;
}
// quit unless Qt Widgets GUI should be shown
if (!qtConfigArgs.qtWidgetsGuiArg().isPresent()) {
return 0;
return EXIT_SUCCESS;
}
// handle override for config dir
@ -206,16 +224,18 @@ int runApplication(int argc, const char *const *argv)
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, QString::fromLocal8Bit(configPathDir));
}
// check whether runApplication() has been called for the first time
static auto firstRun = true;
// do first-time initializations
if (firstRun) {
firstRun = false;
// do first-time initializations
SET_QT_APPLICATION_INFO;
QApplication application(argc, const_cast<char **>(argv));
auto application = QApplication(argc, const_cast<char **>(argv));
QGuiApplication::setQuitOnLastWindowClosed(false);
SingleInstance singleInstance(argc, argv, newInstanceArg.isPresent());
// stop possibly running instance if --replace is present
if (replaceArg.isPresent()) {
const char *const argv[] = { parser.executable(), quitArg.name() };
SingleInstance::passArgsToRunningInstance(2, argv, SingleInstance::applicationId(), true);
}
auto singleInstance = SingleInstance(argc, argv, newInstanceArg.isPresent(), replaceArg.isPresent());
networkAccessManager().setParent(&singleInstance);
QObject::connect(&singleInstance, &SingleInstance::newInstance, &runApplication);
Settings::restore();
@ -233,10 +253,10 @@ int runApplication(int argc, const char *const *argv)
if (!settings.error.isEmpty()) {
QMessageBox::critical(nullptr, QCoreApplication::applicationName(), settings.error);
}
SyncthingLauncher launcher;
auto launcher = SyncthingLauncher();
SyncthingLauncher::setMainInstance(&launcher);
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
SyncthingService service;
auto service = SyncthingService();
SyncthingService::setMainInstance(&service);
settings.systemd.setupService(service);
QObject::connect(&service, &SyncthingService::errorOccurred, &handleSystemdServiceError);
@ -246,6 +266,8 @@ int runApplication(int argc, const char *const *argv)
}
// init Syncthing Tray and immediately shutdown on failure
auto parent = QObject();
parentObject = &parent;
if (const auto res = initSyncthingTray(windowedArg.isPresent(), waitForTrayArg.isPresent(), connectionArg)) {
shutdownSyncthingTray();
return res;

View File

@ -4,9 +4,11 @@
#include <c++utilities/io/ansiescapecodes.h>
#include <QCoreApplication>
#include <QFile>
#include <QLocalServer>
#include <QLocalSocket>
#include <QStringBuilder>
#include <QThread>
#include <iostream>
#include <memory>
@ -64,57 +66,81 @@ static QString getCurrentProcessSIDAsString()
}
#endif
SingleInstance::SingleInstance(int argc, const char *const *argv, bool newInstance, QObject *parent)
SingleInstance::SingleInstance(int argc, const char *const *argv, bool skipSingleInstanceBehavior, bool skipPassing, QObject *parent)
: QObject(parent)
, m_server(nullptr)
{
if (newInstance) {
// just do nothing if supposed to skip single instance behavior
if (skipSingleInstanceBehavior) {
return;
}
// check for running instance
static const auto appId = QString(QCoreApplication::applicationName() % QChar('-') % QCoreApplication::organizationName() % QChar('-') %
// check for running instance; if there is one pass parameters and exit
static const auto appId = applicationId();
if (!skipPassing && passArgsToRunningInstance(argc, argv, appId)) {
std::exit(EXIT_SUCCESS);
}
// create local server; at this point no previous instance is running anymore
// -> cleanup possible leftover (previous instance might have crashed)
QLocalServer::removeServer(appId);
// -> setup server
m_server = new QLocalServer(this);
connect(m_server, &QLocalServer::newConnection, this, &SingleInstance::handleNewConnection);
if (!m_server->listen(appId)) {
cerr << Phrases::Error << "Unable to launch as single instance application as " << appId.toStdString() << Phrases::EndFlush;
} else {
cerr << Phrases::Info << "Single instance application ID: " << appId.toStdString() << Phrases::EndFlush;
}
}
const QString &SingleInstance::applicationId()
{
static const auto id = QString(QCoreApplication::applicationName() % QChar('-') % QCoreApplication::organizationName() % QChar('-') %
#ifdef Q_OS_WINDOWS
getCurrentProcessSIDAsString()
#else
QString::number(getuid())
#endif
);
passArgsToRunningInstance(argc, argv, appId);
// no previous instance running
// -> however, previous server instance might not have been cleaned up dute to crash
QLocalServer::removeServer(appId);
// -> start server
m_server = new QLocalServer(this);
connect(m_server, &QLocalServer::newConnection, this, &SingleInstance::handleNewConnection);
if (!m_server->listen(appId)) {
cerr << Phrases::Error << "Unable to launch as single instance application as " << appId.toStdString() << Phrases::EndFlush;
}
return id;
}
void SingleInstance::passArgsToRunningInstance(int argc, const char *const *argv, const QString &appId)
bool SingleInstance::passArgsToRunningInstance(int argc, const char *const *argv, const QString &appId, bool waitUntilGone)
{
QLocalSocket socket;
socket.connectToServer(appId, QLocalSocket::ReadWrite);
if (socket.waitForConnected(1000)) {
cerr << Phrases::Info << "Application already running, sending args to previous instance" << Phrases::EndFlush;
if (argc >= 0 && argc <= 0xFFFF) {
char buffer[2];
BE::getBytes(static_cast<std::uint16_t>(argc), buffer);
socket.write(buffer, 2);
*buffer = '\0';
for (const char *const *end = argv + argc; argv != end; ++argv) {
socket.write(*argv);
socket.write(buffer, 1);
}
} else {
cerr << Phrases::Error << "Unable to pass the specified number of arguments" << Phrases::EndFlush;
}
socket.flush();
socket.close();
exit(0);
if (argc < 0 || argc > 0xFFFF) {
cerr << Phrases::Error << "Unable to pass the specified number of arguments" << Phrases::EndFlush;
return false;
}
auto socket = QLocalSocket();
socket.connectToServer(appId, QLocalSocket::ReadWrite);
const auto fullServerName = socket.fullServerName();
if (!socket.waitForConnected(1000)) {
return false;
}
cerr << Phrases::Info << "Application already running, sending args to previous instance" << Phrases::EndFlush;
char buffer[2];
BE::getBytes(static_cast<std::uint16_t>(argc), buffer);
auto error = socket.write(buffer, 2) < 0;
*buffer = '\0';
for (const char *const *end = argv + argc; argv != end && !error; ++argv) {
error = socket.write(*argv) < 0 || socket.write(buffer, 1) < 0;
}
error = error || !socket.flush();
socket.disconnectFromServer();
if (socket.state() != QLocalSocket::UnconnectedState) {
error = !socket.waitForDisconnected(1000) || error;
}
if (error) {
cerr << Phrases::Error << "Unable to pass args to previous instance: " << socket.errorString().toStdString() << Phrases::EndFlush;
}
if (waitUntilGone) {
cerr << Phrases::Info << "Waiting for previous instance to shutdown" << Phrases::EndFlush;
while (QFile::exists(fullServerName)) {
QThread::msleep(500);
}
}
return !error;
}
void SingleInstance::handleNewConnection()
@ -142,7 +168,7 @@ void SingleInstance::readArgs()
// reconstruct argc and argv array
const auto argc = BE::toUInt16(argData.get());
vector<const char *> args;
auto args = vector<const char *>();
args.reserve(argc + 1);
for (const char *argv = argData.get() + 2, *end = argData.get() + argDataSize, *i = argv; i != end && *argv;) {
if (!*i) {

View File

@ -14,18 +14,21 @@ namespace QtGui {
class SingleInstance : public QObject {
Q_OBJECT
public:
SingleInstance(int argc, const char *const *argv, bool newInstance = false, QObject *parent = nullptr);
explicit SingleInstance(
int argc, const char *const *argv, bool skipSingleInstanceBehavior = false, bool skipPassing = false, QObject *parent = nullptr);
Q_SIGNALS:
void newInstance(int argc, const char *const *argv);
public:
static const QString &applicationId();
static bool passArgsToRunningInstance(int argc, const char *const *argv, const QString &appId, bool waitUntilGone = false);
private Q_SLOTS:
void handleNewConnection();
void readArgs();
private:
void passArgsToRunningInstance(int argc, const char *const *argv, const QString &appId);
QLocalServer *m_server;
};
} // namespace QtGui