From 412855ae3d967ff81a383688397c4d9448a4ee15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 30 Oct 2016 02:37:38 +0100 Subject: NOISSUE refactor window management and launch, make MultiMC a single instance application. --- CMakeLists.txt | 1 + application/CMakeLists.txt | 2 +- application/InstanceWindow.cpp | 11 +- application/InstanceWindow.h | 7 - application/LaunchController.cpp | 17 +- application/MainWindow.cpp | 88 ++--------- application/MainWindow.h | 10 -- application/MultiMC.cpp | 159 ++++++++++++++++++- application/MultiMC.h | 57 ++++--- application/main.cpp | 37 +---- libraries/LocalPeer/CMakeLists.txt | 29 ++++ libraries/LocalPeer/include/LocalPeer.h | 97 ++++++++++++ libraries/LocalPeer/src/LocalPeer.cpp | 236 ++++++++++++++++++++++++++++ libraries/LocalPeer/src/LockedFile.cpp | 193 +++++++++++++++++++++++ libraries/LocalPeer/src/LockedFile.h | 77 +++++++++ libraries/LocalPeer/src/LockedFile_unix.cpp | 114 ++++++++++++++ libraries/LocalPeer/src/LockedFile_win.cpp | 205 ++++++++++++++++++++++++ libraries/README.md | 9 +- 18 files changed, 1164 insertions(+), 185 deletions(-) create mode 100644 libraries/LocalPeer/CMakeLists.txt create mode 100644 libraries/LocalPeer/include/LocalPeer.h create mode 100644 libraries/LocalPeer/src/LocalPeer.cpp create mode 100644 libraries/LocalPeer/src/LockedFile.cpp create mode 100644 libraries/LocalPeer/src/LockedFile.h create mode 100644 libraries/LocalPeer/src/LockedFile_unix.cpp create mode 100644 libraries/LocalPeer/src/LockedFile_win.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e20d375a..25708418 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,7 @@ add_subdirectory(libraries/xz-embedded) # xz compression add_subdirectory(libraries/pack200) # java pack200 compression add_subdirectory(libraries/rainbow) # Qt extension for colors add_subdirectory(libraries/iconfix) # fork of Qt's QIcon loader +add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions ############################### Built Artifacts ############################### diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index 59a51ce7..4a7d644d 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -322,7 +322,7 @@ qt5_add_resources(MULTIMC_RESOURCES ${MULTIMC_QRCS}) # Add executable add_executable(MultiMC MACOSX_BUNDLE WIN32 ${MULTIMC_SOURCES} ${MULTIMC_UI} ${MULTIMC_RESOURCES} ${MULTIMC_RCS}) -target_link_libraries(MultiMC MultiMC_gui ${QUAZIP_LIBRARIES} hoedown rainbow) +target_link_libraries(MultiMC MultiMC_gui ${QUAZIP_LIBRARIES} hoedown rainbow LocalPeer) if(APPLE) find_library(OSX_CORE_FOUNDATION CoreFoundation) diff --git a/application/InstanceWindow.cpp b/application/InstanceWindow.cpp index 7b3e1d10..1c7731bd 100644 --- a/application/InstanceWindow.cpp +++ b/application/InstanceWindow.cpp @@ -174,11 +174,6 @@ void InstanceWindow::closeEvent(QCloseEvent *event) MMC->settings()->set("ConsoleWindowGeometry", saveGeometry().toBase64()); emit isClosing(); event->accept(); - if(m_shouldQuit) - { - // this needs to be delayed so we don't do horrible things - QMetaObject::invokeMethod(MMC, "quit", Qt::QueuedConnection); - } } bool InstanceWindow::saveAll() @@ -203,11 +198,7 @@ void InstanceWindow::on_btnKillMinecraft_clicked() // FIXME: duplicate logic between MainWindow and InstanceWindow else if(saveAll()) { - m_launchController.reset(new LaunchController()); - m_launchController->setInstance(m_instance); - m_launchController->setOnline(true); - m_launchController->setParentWidget(this); - m_launchController->start(); + MMC->launch(m_instance, true, nullptr); } } diff --git a/application/InstanceWindow.h b/application/InstanceWindow.h index 1a6bc8c3..5cbced03 100644 --- a/application/InstanceWindow.h +++ b/application/InstanceWindow.h @@ -36,11 +36,6 @@ public: QString instanceId(); - void setQuitOnClose(bool shouldQuit = false) - { - m_shouldQuit = shouldQuit; - } - // save all settings and changes (prepare for launch) bool saveAll(); @@ -68,9 +63,7 @@ private: private: std::shared_ptr m_proc; - unique_qobject_ptr m_launchController; InstancePtr m_instance; - bool m_shouldQuit = false; bool m_doNotSave = false; PageContainer *m_container = nullptr; QPushButton *m_closeButton = nullptr; diff --git a/application/LaunchController.cpp b/application/LaunchController.cpp index bc7f976a..4dce6708 100644 --- a/application/LaunchController.cpp +++ b/application/LaunchController.cpp @@ -205,21 +205,10 @@ void LaunchController::launchInstance() return; } - auto mainWindow = qobject_cast(m_parentWidget); - auto instanceWindow = qobject_cast(m_parentWidget); - if(mainWindow) + auto console = qobject_cast(m_parentWidget); + if(!console) { - m_console = mainWindow->showInstanceWindow(m_instance); - } - else if(instanceWindow) - { - // NOOP - } - else - { - // this is used when launching directly from command line - m_console = new InstanceWindow(m_instance); - m_console->setQuitOnClose(true); + MMC->showInstanceWindow(m_instance); } connect(m_launcher.get(), &LaunchTask::readyForLaunch, this, &LaunchController::readyForLaunch); diff --git a/application/MainWindow.cpp b/application/MainWindow.cpp index e07686dd..85484700 100644 --- a/application/MainWindow.cpp +++ b/application/MainWindow.cpp @@ -691,7 +691,7 @@ void MainWindow::updateToolsMenu() QAction *normalLaunch = launchMenu->addAction(tr("Launch")); connect(normalLaunch, &QAction::triggered, [this]() { - launch(m_selectedInstance); + MMC->launch(m_selectedInstance); }); launchMenu->addSeparator()->setText(tr("Profilers")); for (auto profiler : MMC->profilers().values()) @@ -707,7 +707,7 @@ void MainWindow::updateToolsMenu() { connect(profilerAction, &QAction::triggered, [this, profiler]() { - launch(m_selectedInstance, true, profiler.get()); + MMC->launch(m_selectedInstance, true, profiler.get()); }); } } @@ -953,7 +953,7 @@ void MainWindow::downloadUpdates(GoUpdate::Status status) { qDebug() << "Downloading updates."; ProgressDialog updateDlg(this); - status.rootPath = MMC->rootPath; + status.rootPath = MMC->root(); auto dlPath = FS::PathCombine(MMC->root(), "update", "XXXXXX"); if (!FS::ensureFilePathExists(dlPath)) @@ -1004,8 +1004,8 @@ void MainWindow::waitForMinecraftVersions() if (!MMC->minecraftlist()->isLoaded() && m_versionLoadTask && m_versionLoadTask->isRunning()) { QEventLoop waitLoop; - waitLoop.connect(m_versionLoadTask, SIGNAL(failed(QString)), SLOT(quit())); - waitLoop.connect(m_versionLoadTask, SIGNAL(succeeded()), SLOT(quit())); + waitLoop.connect(m_versionLoadTask, &Task::failed, &waitLoop, &QEventLoop::quit); + waitLoop.connect(m_versionLoadTask, &Task::succeeded, &waitLoop, &QEventLoop::quit); waitLoop.exec(); } } @@ -1261,62 +1261,24 @@ void MainWindow::on_actionSettings_triggered() update(); } -InstanceWindow *MainWindow::showInstanceWindow(InstancePtr instance, QString page) -{ - if(!instance) - return nullptr; - auto id = instance->id(); - InstanceWindow * window = nullptr; - - auto iter = m_instanceWindows.find(id); - if(iter != m_instanceWindows.end()) - { - window = *iter; - window->raise(); - window->activateWindow(); - } - else - { - window = new InstanceWindow(instance); - m_instanceWindows[id] = window; - connect(window, &InstanceWindow::isClosing, this, &MainWindow::on_instanceWindowClose); - } - if(!page.isEmpty()) - { - window->selectPage(page); - } - return window; -} - -void MainWindow::on_instanceWindowClose() -{ - auto senderWindow = qobject_cast(QObject::sender()); - if(!senderWindow) - { - return; - } - m_instanceWindows.remove(senderWindow->instanceId()); -} - - void MainWindow::on_actionInstanceSettings_triggered() { - showInstanceWindow(m_selectedInstance, "settings"); + MMC->showInstanceWindow(m_selectedInstance, "settings"); } void MainWindow::on_actionEditInstNotes_triggered() { - showInstanceWindow(m_selectedInstance, "notes"); + MMC->showInstanceWindow(m_selectedInstance, "notes"); } void MainWindow::on_actionEditInstance_triggered() { - showInstanceWindow(m_selectedInstance); + MMC->showInstanceWindow(m_selectedInstance); } void MainWindow::on_actionScreenshots_triggered() { - showInstanceWindow(m_selectedInstance, "screenshots"); + MMC->showInstanceWindow(m_selectedInstance, "screenshots"); } void MainWindow::on_actionManageAccounts_triggered() @@ -1440,14 +1402,14 @@ void MainWindow::instanceActivated(QModelIndex index) if (!inst) return; - launch(inst); + MMC->launch(inst); } void MainWindow::on_actionLaunchInstance_triggered() { if (m_selectedInstance) { - launch(m_selectedInstance); + MMC->launch(m_selectedInstance); } } @@ -1455,33 +1417,7 @@ void MainWindow::on_actionLaunchInstanceOffline_triggered() { if (m_selectedInstance) { - launch(m_selectedInstance, false); - } -} - -void MainWindow::launch(InstancePtr instance, bool online, BaseProfilerFactory *profiler) -{ - if(instance->canLaunch()) - { - // FIXME: duplicate logic between MainWindow and InstanceWindow - auto window = m_instanceWindows.find(instance->id()); - if(window != m_instanceWindows.end()) - { - if(!(*window)->saveAll()) - { - return; - } - } - m_launchController.reset(new LaunchController()); - m_launchController->setInstance(instance); - m_launchController->setOnline(online); - m_launchController->setParentWidget(this); - m_launchController->setProfiler(profiler); - m_launchController->start(); - } - else if (instance->isRunning()) - { - showInstanceWindow(instance, "console"); + MMC->launch(m_selectedInstance, false); } } diff --git a/application/MainWindow.h b/application/MainWindow.h index d0660676..e499f162 100644 --- a/application/MainWindow.h +++ b/application/MainWindow.h @@ -37,7 +37,6 @@ class MinecraftLauncher; class BaseProfilerFactory; class GroupView; class ServerStatus; -class InstanceWindow; class MainWindow : public QMainWindow { @@ -55,8 +54,6 @@ public: void checkSetDefaultJava(); void checkInstancePathForProblems(); - InstanceWindow *showInstanceWindow(InstancePtr instance, QString page = QString()); - private slots: void onCatToggled(bool); @@ -162,8 +159,6 @@ private slots: */ void downloadUpdates(GoUpdate::Status status); - void on_instanceWindowClose(); - private: void setCatBackground(bool enabled); void updateInstanceToolIcon(QString new_icon); @@ -174,7 +169,6 @@ private: void instanceFromVersion(QString instName, QString instGroup, QString instIcon, BaseVersionPtr version); void instanceFromZipPack(QString instName, QString instGroup, QString instIcon, QUrl url); void finalizeInstance(InstancePtr inst); - void launch(InstancePtr instance, bool online = true, BaseProfilerFactory *profiler = nullptr); private: std::unique_ptr ui; @@ -194,14 +188,10 @@ private: unique_qobject_ptr skin_download_job; unique_qobject_ptr m_newsChecker; unique_qobject_ptr m_notificationChecker; - unique_qobject_ptr m_launchController; InstancePtr m_selectedInstance; QString m_currentInstIcon; // managed by the application object Task *m_versionLoadTask; - - // map from instance ID to its window - QMap m_instanceWindows; }; diff --git a/application/MultiMC.cpp b/application/MultiMC.cpp index 927468b6..41f5281c 100644 --- a/application/MultiMC.cpp +++ b/application/MultiMC.cpp @@ -1,5 +1,7 @@ #include "MultiMC.h" #include "BuildConfig.h" +#include "MainWindow.h" +#include "InstanceWindow.h" #include "pages/BasePageProvider.h" #include "pages/global/MultiMCPage.h" #include "pages/global/MinecraftPage.h" @@ -59,6 +61,7 @@ #include #include #include +#include #if defined Q_OS_WIN32 #ifndef WIN32_LEAN_AND_MEAN @@ -159,6 +162,7 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) return; } } + m_instanceIdToLaunch = args["launch"].toString(); QString origcwdPath = QDir::currentPath(); QString binPath = applicationDirPath(); @@ -179,21 +183,32 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) adjustedBy += "Fallback to binary path " + dataPath; } - instanceIdToLaunch = args["launch"].toString(); - if (!FS::ensureFolderPathExists(dataPath) || !QDir::setCurrent(dataPath)) { // BAD STUFF. WHAT DO? - initLogger(); - qCritical() << "Failed to set work path. Will exit. NOW."; m_status = MultiMC::Failed; return; } + m_peerInstance = new LocalPeer(this, ApplicationId::fromPathAndVersion(dataPath, BuildConfig.printableVersionString())); + connect(m_peerInstance, &LocalPeer::messageReceived, this, &MultiMC::messageReceived); + if(m_peerInstance->isClient()) + { + if(m_instanceIdToLaunch.isEmpty()) + { + m_peerInstance->sendMessage("activate", 2000); + } + else + { + m_peerInstance->sendMessage(m_instanceIdToLaunch, 2000); + } + quit(); + return; + } // in test mode, root path is the same as the binary path. #ifdef Q_OS_LINUX QDir foo(FS::PathCombine(binPath, "..")); - rootPath = foo.absolutePath(); + m_rootPath = foo.absolutePath(); #elif defined(Q_OS_WIN32) rootPath = binPath; #elif defined(Q_OS_MAC) @@ -219,10 +234,10 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) qDebug() << "Work dir : " << QDir::currentPath(); } qDebug() << "Binary path : " << binPath; - qDebug() << "Application root path : " << rootPath; - if(!instanceIdToLaunch.isEmpty()) + qDebug() << "Application root path : " << m_rootPath; + if(!m_instanceIdToLaunch.isEmpty()) { - qDebug() << "ID of instance to launch : " << instanceIdToLaunch; + qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch; } // load settings @@ -309,7 +324,22 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) } connect(this, SIGNAL(aboutToQuit()), SLOT(onExit())); + m_status = MultiMC::Initialized; + + setIconTheme(settings()->get("IconTheme").toString()); + setApplicationTheme(settings()->get("ApplicationTheme").toString()); + if(!m_instanceIdToLaunch.isEmpty()) + { + auto inst = instances()->getInstanceById(m_instanceIdToLaunch); + if(inst) + { + minecraftlist(); + launch(inst, true, nullptr); + return; + } + } + showMainWindow(); } MultiMC::~MultiMC() @@ -333,6 +363,22 @@ MultiMC::~MultiMC() #endif } +void MultiMC::messageReceived(const QString& message) +{ + if(message == "activate") + { + showMainWindow(); + } + else + { + auto inst = instances()->getInstanceById(message); + if(inst) + { + launch(inst, true, nullptr); + } + } +} + #ifdef Q_OS_MAC #include "CertWorkaround.h" #endif @@ -1024,4 +1070,101 @@ bool MultiMC::openJsonEditor(const QString &filename) } } +void MultiMC::launch(InstancePtr instance, bool online, BaseProfilerFactory *profiler) +{ + if(instance->canLaunch()) + { + m_launchController.reset(new LaunchController()); + m_launchController->setInstance(instance); + m_launchController->setOnline(online); + m_launchController->setProfiler(profiler); + auto windowIter = m_instanceWindows.find(instance->id()); + if(windowIter != m_instanceWindows.end()) + { + auto window = *windowIter; + if(!window->saveAll()) + { + return; + } + m_launchController->setParentWidget(window); + } + if(m_mainWindow) + { + m_launchController->setParentWidget(m_mainWindow); + } + m_launchController->start(); + } + else if (instance->isRunning()) + { + showInstanceWindow(instance, "console"); + } +} + +MainWindow * MultiMC::showMainWindow() +{ + if(m_mainWindow) + { + m_mainWindow->setWindowState(m_mainWindow->windowState() & ~Qt::WindowMinimized); + m_mainWindow->raise(); + m_mainWindow->activateWindow(); + } + else + { + m_mainWindow = new MainWindow(); + m_mainWindow->restoreState(QByteArray::fromBase64(MMC->settings()->get("MainWindowState").toByteArray())); + m_mainWindow->restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("MainWindowGeometry").toByteArray())); + m_mainWindow->show(); + m_mainWindow->checkSetDefaultJava(); + m_mainWindow->checkInstancePathForProblems(); + } + return m_mainWindow; +} + +InstanceWindow *MultiMC::showInstanceWindow(InstancePtr instance, QString page) +{ + if(!instance) + return nullptr; + auto id = instance->id(); + InstanceWindow * window = nullptr; + + auto iter = m_instanceWindows.find(id); + if(iter != m_instanceWindows.end()) + { + window = *iter; + window->raise(); + window->activateWindow(); + } + else + { + window = new InstanceWindow(instance); + m_instanceWindows[id] = window; + connect(window, &InstanceWindow::isClosing, this, &MultiMC::on_windowClose); + } + if(!page.isEmpty()) + { + window->selectPage(page); + } + return window; +} + +void MultiMC::on_windowClose() +{ + auto instWindow = qobject_cast(QObject::sender()); + if(instWindow) + { + m_instanceWindows.remove(instWindow->instanceId()); + return; + } + auto mainWindow = qobject_cast(QObject::sender()); + if(mainWindow) + { + m_mainWindow = nullptr; + } + // quit when there are no more windows. + if(m_instanceWindows.isEmpty() && !m_mainWindow) + { + quit(); + } +} + #include "MultiMC.moc" diff --git a/application/MultiMC.h b/application/MultiMC.h index 070447fc..663da1a1 100644 --- a/application/MultiMC.h +++ b/application/MultiMC.h @@ -7,8 +7,14 @@ #include #include #include -class FolderInstanceProvider; +#include + +class LaunchController; +class LocalPeer; +class InstanceWindow; +class MainWindow; +class FolderInstanceProvider; class GenericPageProvider; class QFile; class MinecraftVersionList; @@ -36,8 +42,6 @@ class ITheme; class MultiMC : public QApplication { // friends for the purpose of limiting access to deprecated stuff - friend class MultiMCPage; - friend class MainWindow; Q_OBJECT public: enum Status @@ -51,13 +55,12 @@ public: MultiMC(int &argc, char **argv); virtual ~MultiMC(); - // InstanceList, IconList, OneSixFTBInstance, LegacyUpdate, LegacyInstance, MCEditTool, JVisualVM, MinecraftInstance, JProfiler, BaseInstance - std::shared_ptr settings() + std::shared_ptr settings() const { return m_settings; } - std::shared_ptr globalSettingsPages() + std::shared_ptr globalSettingsPages() const { return m_globalSettingsProvider; } @@ -72,6 +75,7 @@ public: void setIconTheme(const QString& name); std::vector getValidApplicationThemes(); + void setApplicationTheme(const QString& name); // DownloadUpdateTask @@ -86,7 +90,6 @@ public: std::shared_ptr liteloaderlist(); std::shared_ptr javalist(); - // APPLICATION ONLY std::shared_ptr instances() const { return m_instances; @@ -102,46 +105,44 @@ public: return m_icons; } - // APPLICATION ONLY std::shared_ptr accounts() const { return m_accounts; } - // APPLICATION ONLY Status status() const { return m_status; } - // APPLICATION ONLY const QMap> &profilers() const { return m_profilers; } - // APPLICATION ONLY const QMap> &tools() const { return m_tools; } - // APPLICATION ONLY + /// this is the root of the 'installation'. Used for automatic updates + const QString &root() + { + return m_rootPath; + } + + // install updates now. void installUpdates(const QString updateFilesDir, GoUpdate::OperationList operations); /*! - * Opens a json file using either a system default editor, or, if note empty, the editor + * Opens a json file using either a system default editor, or, if not empty, the editor * specified in the settings */ bool openJsonEditor(const QString &filename); -protected: /* to be removed! */ - // FIXME: remove. used by MainWindow to create application update tasks - /// this is the root of the 'installation'. Used for automatic updates - const QString &root() - { - return rootPath; - } + InstanceWindow *showInstanceWindow(InstancePtr instance, QString page = QString()); + MainWindow *showMainWindow(); + void launch(InstancePtr instance, bool online = true, BaseProfilerFactory *profiler = nullptr); private slots: /** @@ -149,6 +150,10 @@ private slots: */ void onExit(); + void on_windowClose(); + + void messageReceived(const QString & message); + private: void initLogger(); void initIcons(); @@ -160,6 +165,7 @@ private: private: QDateTime startTime; + unique_qobject_ptr m_launchController; std::shared_ptr m_qt_translator; std::shared_ptr m_mmc_translator; std::shared_ptr m_settings; @@ -180,12 +186,19 @@ private: QMap> m_profilers; QMap> m_tools; - QString rootPath; + QString m_rootPath; Status m_status = MultiMC::Failed; + // used on Windows to attach the standard IO streams bool consoleAttached = false; + + // map from instance ID to its window + QMap m_instanceWindows; + // main window, if any + MainWindow * m_mainWindow = nullptr; + LocalPeer * m_peerInstance = nullptr; public: - QString instanceIdToLaunch; + QString m_instanceIdToLaunch; std::unique_ptr logFile; }; diff --git a/application/main.cpp b/application/main.cpp index c6847c5d..fde5eba7 100644 --- a/application/main.cpp +++ b/application/main.cpp @@ -4,41 +4,6 @@ #include #include -int launchMainWindow(MultiMC &app) -{ - MainWindow mainWin; - mainWin.restoreState(QByteArray::fromBase64(MMC->settings()->get("MainWindowState").toByteArray())); - mainWin.restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("MainWindowGeometry").toByteArray())); - mainWin.show(); - mainWin.checkSetDefaultJava(); - mainWin.checkInstancePathForProblems(); - return app.exec(); -} - -int launchInstance(MultiMC &app, InstancePtr inst) -{ - app.minecraftlist(); - LaunchController launchController; - launchController.setInstance(inst); - launchController.setOnline(true); - QMetaObject::invokeMethod(&launchController, "start", Qt::QueuedConnection); - return app.exec(); -} - -int main_gui(MultiMC &app) -{ - app.setIconTheme(MMC->settings()->get("IconTheme").toString()); - app.setApplicationTheme(MMC->settings()->get("ApplicationTheme").toString()); - - // show main window - auto inst = app.instances()->getInstanceById(app.instanceIdToLaunch); - if(inst) - { - return launchInstance(app, inst); - } - return launchMainWindow(app); -} - int main(int argc, char *argv[]) { // initialize Qt @@ -59,7 +24,7 @@ int main(int argc, char *argv[]) switch (app.status()) { case MultiMC::Initialized: - return main_gui(app); + return app.exec(); case MultiMC::Failed: return 1; case MultiMC::Succeeded: diff --git a/libraries/LocalPeer/CMakeLists.txt b/libraries/LocalPeer/CMakeLists.txt new file mode 100644 index 00000000..99e3fe4d --- /dev/null +++ b/libraries/LocalPeer/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.1) +project(LocalPeer) + +find_package(Qt5Core REQUIRED QUIET) +find_package(Qt5Network REQUIRED QUIET) + +set(SINGLE_SOURCES +src/LocalPeer.cpp +src/LockedFile.cpp +src/LockedFile.h +include/LocalPeer.h +) + +if(UNIX) + list(APPEND SINGLE_SOURCES + src/LockedFile_unix.cpp + ) +endif() + +if(WIN32) + list(APPEND SINGLE_SOURCES + src/LockedFile_win.cpp + ) +endif() + +add_library(LocalPeer STATIC ${SINGLE_SOURCES}) +target_include_directories(LocalPeer PUBLIC include) + +qt5_use_modules(LocalPeer Core Network) diff --git a/libraries/LocalPeer/include/LocalPeer.h b/libraries/LocalPeer/include/LocalPeer.h new file mode 100644 index 00000000..940bfcbd --- /dev/null +++ b/libraries/LocalPeer/include/LocalPeer.h @@ -0,0 +1,97 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once +#include +#include +#include + + +class QLocalServer; +class LockedFile; + +class ApplicationId +{ +public: /* methods */ + // traditional app = installed system wide and used in a multi-user environment + static ApplicationId fromTraditionalApp(); + // ID based on a path with all the application data (no two instances with the same data path should run) + static ApplicationId fromPathAndVersion(const QString & dataPath, const QString & version); + // fully custom ID + static ApplicationId fromCustomId(const QString & id); + + QString toString() + { + return m_id; + } + +private: /* methods */ + ApplicationId(const QString & value) + { + m_id = value; + } + +private: /* data */ + QString m_id; +}; + +class LocalPeer : public QObject +{ + Q_OBJECT + +public: + LocalPeer(QObject *parent, const ApplicationId &appId); + ~LocalPeer(); + bool isClient(); + bool sendMessage(const QString &message, int timeout); + ApplicationId applicationId() const; + +Q_SIGNALS: + void messageReceived(const QString &message); + +protected Q_SLOTS: + void receiveConnection(); + +protected: + ApplicationId id; + QString socketName; + std::unique_ptr server; + std::unique_ptr lockFile; +}; diff --git a/libraries/LocalPeer/src/LocalPeer.cpp b/libraries/LocalPeer/src/LocalPeer.cpp new file mode 100644 index 00000000..db0c73e5 --- /dev/null +++ b/libraries/LocalPeer/src/LocalPeer.cpp @@ -0,0 +1,236 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include "LocalPeer.h" +#include +#include +#include +#include +#include +#include +#include "LockedFile.h" + +#if defined(Q_OS_WIN) +#include +#include +typedef BOOL(WINAPI*PProcessIdToSessionId)(DWORD,DWORD*); +static PProcessIdToSessionId pProcessIdToSessionId = 0; +#endif +#if defined(Q_OS_UNIX) +#include +#include +#endif + +#include +#include +#include + +static const char* ack = "ack"; + +ApplicationId ApplicationId::fromTraditionalApp() +{ + QString protoId = QCoreApplication::applicationFilePath(); +#if defined(Q_OS_WIN) + protoId = protoId.toLower(); +#endif + auto prefix = protoId.section(QLatin1Char('/'), -1); + prefix.remove(QRegExp("[^a-zA-Z]")); + prefix.truncate(6); + QByteArray idc = protoId.toUtf8(); + quint16 idNum = qChecksum(idc.constData(), idc.size()); + auto socketName = QLatin1String("qtsingleapp-") + prefix + QLatin1Char('-') + QString::number(idNum, 16); +#if defined(Q_OS_WIN) + if (!pProcessIdToSessionId) + { + QLibrary lib("kernel32"); + pProcessIdToSessionId = (PProcessIdToSessionId)lib.resolve("ProcessIdToSessionId"); + } + if (pProcessIdToSessionId) + { + DWORD sessionId = 0; + pProcessIdToSessionId(GetCurrentProcessId(), &sessionId); + socketName += QLatin1Char('-') + QString::number(sessionId, 16); + } +#else + socketName += QLatin1Char('-') + QString::number(::getuid(), 16); +#endif + return ApplicationId(socketName); +} + +ApplicationId ApplicationId::fromPathAndVersion(const QString& dataPath, const QString& version) +{ + QCryptographicHash shasum(QCryptographicHash::Algorithm::Sha1); + QString result = dataPath + QLatin1Char('-') + version; + shasum.addData(result.toUtf8()); + return ApplicationId(QLatin1String("qtsingleapp-") + QString::fromLatin1(shasum.result().toHex())); +} + +ApplicationId ApplicationId::fromCustomId(const QString& id) +{ + return ApplicationId(QLatin1String("qtsingleapp-") + id); +} + +LocalPeer::LocalPeer(QObject * parent, const ApplicationId &appId) + : QObject(parent), id(appId) +{ + socketName = id.toString(); + server.reset(new QLocalServer()); + QString lockName = QDir(QDir::tempPath()).absolutePath() + QLatin1Char('/') + socketName + QLatin1String("-lockfile"); + lockFile.reset(new LockedFile(lockName)); + lockFile->open(QIODevice::ReadWrite); +} + +LocalPeer::~LocalPeer() +{ +} + +ApplicationId LocalPeer::applicationId() const +{ + return id; +} + +bool LocalPeer::isClient() +{ + if (lockFile->isLocked()) + return false; + + if (!lockFile->lock(LockedFile::WriteLock, false)) + return true; + + bool res = server->listen(socketName); +#if defined(Q_OS_UNIX) + // ### Workaround + if (!res && server->serverError() == QAbstractSocket::AddressInUseError) { + QFile::remove(QDir::cleanPath(QDir::tempPath())+QLatin1Char('/')+socketName); + res = server->listen(socketName); + } +#endif + if (!res) + qWarning("QtSingleCoreApplication: listen on local socket failed, %s", qPrintable(server->errorString())); + QObject::connect(server.get(), SIGNAL(newConnection()), SLOT(receiveConnection())); + return false; +} + + +bool LocalPeer::sendMessage(const QString &message, int timeout) +{ + if (!isClient()) + return false; + + QLocalSocket socket; + bool connOk = false; + for(int i = 0; i < 2; i++) { + // Try twice, in case the other instance is just starting up + socket.connectToServer(socketName); + connOk = socket.waitForConnected(timeout/2); + if (connOk || i) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } + if (!connOk) + { + return false; + } + + QByteArray uMsg(message.toUtf8()); + QDataStream ds(&socket); + + ds.writeBytes(uMsg.constData(), uMsg.size()); + if(!socket.waitForBytesWritten(timeout)) + { + return false; + } + + // wait for 'ack' + if(!socket.waitForReadyRead(timeout)) + { + return false; + } + + // make sure we got 'ack' + if(!(socket.read(qstrlen(ack)) == ack)) + { + return false; + } + return true; +} + + +void LocalPeer::receiveConnection() +{ + QLocalSocket* socket = server->nextPendingConnection(); + if (!socket) + { + return; + } + + while (socket->bytesAvailable() < (int)sizeof(quint32)) + { + socket->waitForReadyRead(); + } + QDataStream ds(socket); + QByteArray uMsg; + quint32 remaining; + ds >> remaining; + uMsg.resize(remaining); + int got = 0; + char* uMsgBuf = uMsg.data(); + do + { + got = ds.readRawData(uMsgBuf, remaining); + remaining -= got; + uMsgBuf += got; + } while (remaining && got >= 0 && socket->waitForReadyRead(2000)); + if (got < 0) + { + qWarning("QtLocalPeer: Message reception failed %s", socket->errorString().toLatin1().constData()); + delete socket; + return; + } + QString message(QString::fromUtf8(uMsg)); + socket->write(ack, qstrlen(ack)); + socket->waitForBytesWritten(1000); + socket->waitForDisconnected(1000); // make sure client reads ack + delete socket; + emit messageReceived(message); //### (might take a long time to return) +} diff --git a/libraries/LocalPeer/src/LockedFile.cpp b/libraries/LocalPeer/src/LockedFile.cpp new file mode 100644 index 00000000..a4951bfe --- /dev/null +++ b/libraries/LocalPeer/src/LockedFile.cpp @@ -0,0 +1,193 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "LockedFile.h" + +/*! + \class QtLockedFile + + \brief The QtLockedFile class extends QFile with advisory locking + functions. + + A file may be locked in read or write mode. Multiple instances of + \e QtLockedFile, created in multiple processes running on the same + machine, may have a file locked in read mode. Exactly one instance + may have it locked in write mode. A read and a write lock cannot + exist simultaneously on the same file. + + The file locks are advisory. This means that nothing prevents + another process from manipulating a locked file using QFile or + file system functions offered by the OS. Serialization is only + guaranteed if all processes that access the file use + QLockedFile. Also, while holding a lock on a file, a process + must not open the same file again (through any API), or locks + can be unexpectedly lost. + + The lock provided by an instance of \e QtLockedFile is released + whenever the program terminates. This is true even when the + program crashes and no destructors are called. +*/ + +/*! \enum QtLockedFile::LockMode + + This enum describes the available lock modes. + + \value ReadLock A read lock. + \value WriteLock A write lock. + \value NoLock Neither a read lock nor a write lock. +*/ + +/*! + Constructs an unlocked \e QtLockedFile object. This constructor + behaves in the same way as \e QFile::QFile(). + + \sa QFile::QFile() +*/ +LockedFile::LockedFile() + : QFile() +{ +#ifdef Q_OS_WIN + wmutex = 0; + rmutex = 0; +#endif + m_lock_mode = NoLock; +} + +/*! + Constructs an unlocked QtLockedFile object with file \a name. This + constructor behaves in the same way as \e QFile::QFile(const + QString&). + + \sa QFile::QFile() +*/ +LockedFile::LockedFile(const QString &name) + : QFile(name) +{ +#ifdef Q_OS_WIN + wmutex = 0; + rmutex = 0; +#endif + m_lock_mode = NoLock; +} + +/*! +Opens the file in OpenMode \a mode. + +This is identical to QFile::open(), with the one exception that the +Truncate mode flag is disallowed. Truncation would conflict with the +advisory file locking, since the file would be modified before the +write lock is obtained. If truncation is required, use resize(0) +after obtaining the write lock. + +Returns true if successful; otherwise false. + +\sa QFile::open(), QFile::resize() +*/ +bool LockedFile::open(OpenMode mode) +{ + if (mode & QIODevice::Truncate) { + qWarning("QtLockedFile::open(): Truncate mode not allowed."); + return false; + } + return QFile::open(mode); +} + +/*! + Returns \e true if this object has a in read or write lock; + otherwise returns \e false. + + \sa lockMode() +*/ +bool LockedFile::isLocked() const +{ + return m_lock_mode != NoLock; +} + +/*! + Returns the type of lock currently held by this object, or \e + QtLockedFile::NoLock. + + \sa isLocked() +*/ +LockedFile::LockMode LockedFile::lockMode() const +{ + return m_lock_mode; +} + +/*! + \fn bool QtLockedFile::lock(LockMode mode, bool block = true) + + Obtains a lock of type \a mode. The file must be opened before it + can be locked. + + If \a block is true, this function will block until the lock is + aquired. If \a block is false, this function returns \e false + immediately if the lock cannot be aquired. + + If this object already has a lock of type \a mode, this function + returns \e true immediately. If this object has a lock of a + different type than \a mode, the lock is first released and then a + new lock is obtained. + + This function returns \e true if, after it executes, the file is + locked by this object, and \e false otherwise. + + \sa unlock(), isLocked(), lockMode() +*/ + +/*! + \fn bool QtLockedFile::unlock() + + Releases a lock. + + If the object has no lock, this function returns immediately. + + This function returns \e true if, after it executes, the file is + not locked by this object, and \e false otherwise. + + \sa lock(), isLocked(), lockMode() +*/ + +/*! + \fn QtLockedFile::~QtLockedFile() + + Destroys the \e QtLockedFile object. If any locks were held, they + are released. +*/ diff --git a/libraries/LocalPeer/src/LockedFile.h b/libraries/LocalPeer/src/LockedFile.h new file mode 100644 index 00000000..8c178250 --- /dev/null +++ b/libraries/LocalPeer/src/LockedFile.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once + +#include +#ifdef Q_OS_WIN +#include +#endif + +class LockedFile : public QFile +{ +public: + enum LockMode { NoLock = 0, ReadLock, WriteLock }; + + LockedFile(); + LockedFile(const QString &name); + ~LockedFile(); + + bool open(OpenMode mode); + + bool lock(LockMode mode, bool block = true); + bool unlock(); + bool isLocked() const; + LockMode lockMode() const; + + private: + +#ifdef Q_OS_WIN + Qt::HANDLE wmutex; + Qt::HANDLE rmutex; + QVector rmutexes; + QString mutexname; + + Qt::HANDLE getMutexHandle(int idx, bool doCreate); + bool waitMutex(Qt::HANDLE mutex, bool doBlock); +#endif + + LockMode m_lock_mode; +}; diff --git a/libraries/LocalPeer/src/LockedFile_unix.cpp b/libraries/LocalPeer/src/LockedFile_unix.cpp new file mode 100644 index 00000000..4b68916c --- /dev/null +++ b/libraries/LocalPeer/src/LockedFile_unix.cpp @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include +#include + +#include "LockedFile.h" + +bool LockedFile::lock(LockMode mode, bool block) +{ + if (!isOpen()) { + qWarning("QtLockedFile::lock(): file is not opened"); + return false; + } + + if (mode == NoLock) + return unlock(); + + if (mode == m_lock_mode) + return true; + + if (m_lock_mode != NoLock) + unlock(); + + struct flock fl; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; + fl.l_type = (mode == ReadLock) ? F_RDLCK : F_WRLCK; + int cmd = block ? F_SETLKW : F_SETLK; + int ret = fcntl(handle(), cmd, &fl); + + if (ret == -1) { + if (errno != EINTR && errno != EAGAIN) + qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno)); + return false; + } + + + m_lock_mode = mode; + return true; +} + + +bool LockedFile::unlock() +{ + if (!isOpen()) { + qWarning("QtLockedFile::unlock(): file is not opened"); + return false; + } + + if (!isLocked()) + return true; + + struct flock fl; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; + fl.l_type = F_UNLCK; + int ret = fcntl(handle(), F_SETLKW, &fl); + + if (ret == -1) { + qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno)); + return false; + } + + m_lock_mode = NoLock; + return true; +} + +LockedFile::~LockedFile() +{ + if (isOpen()) + unlock(); +} diff --git a/libraries/LocalPeer/src/LockedFile_win.cpp b/libraries/LocalPeer/src/LockedFile_win.cpp new file mode 100644 index 00000000..3d7d5553 --- /dev/null +++ b/libraries/LocalPeer/src/LockedFile_win.cpp @@ -0,0 +1,205 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "LockedFile.h" +#include +#include + +#define MUTEX_PREFIX "QtLockedFile mutex " +// Maximum number of concurrent read locks. Must not be greater than MAXIMUM_WAIT_OBJECTS +#define MAX_READERS MAXIMUM_WAIT_OBJECTS + +Qt::HANDLE LockedFile::getMutexHandle(int idx, bool doCreate) +{ + if (mutexname.isEmpty()) { + QFileInfo fi(*this); + mutexname = QString::fromLatin1(MUTEX_PREFIX) + + fi.absoluteFilePath().toLower(); + } + QString mname(mutexname); + if (idx >= 0) + mname += QString::number(idx); + + Qt::HANDLE mutex; + if (doCreate) { + mutex = CreateMutexW(NULL, FALSE, (TCHAR*)mname.utf16()); + if (!mutex) { + qErrnoWarning("QtLockedFile::lock(): CreateMutex failed"); + return 0; + } + } + else { + OpenMutexW(SYNCHRONIZE | MUTEX_MODIFY_STATE, FALSE, (TCHAR*)mname.utf16()); + if (!mutex) { + if (GetLastError() != ERROR_FILE_NOT_FOUND) + qErrnoWarning("QtLockedFile::lock(): OpenMutex failed"); + return 0; + } + } + return mutex; +} + +bool LockedFile::waitMutex(Qt::HANDLE mutex, bool doBlock) +{ + Q_ASSERT(mutex); + DWORD res = WaitForSingleObject(mutex, doBlock ? INFINITE : 0); + switch (res) { + case WAIT_OBJECT_0: + case WAIT_ABANDONED: + return true; + break; + case WAIT_TIMEOUT: + break; + default: + qErrnoWarning("QtLockedFile::lock(): WaitForSingleObject failed"); + } + return false; +} + + + +bool LockedFile::lock(LockMode mode, bool block) +{ + if (!isOpen()) { + qWarning("QtLockedFile::lock(): file is not opened"); + return false; + } + + if (mode == NoLock) + return unlock(); + + if (mode == m_lock_mode) + return true; + + if (m_lock_mode != NoLock) + unlock(); + + if (!wmutex && !(wmutex = getMutexHandle(-1, true))) + return false; + + if (!waitMutex(wmutex, block)) + return false; + + if (mode == ReadLock) { + int idx = 0; + for (; idx < MAX_READERS; idx++) { + rmutex = getMutexHandle(idx, false); + if (!rmutex || waitMutex(rmutex, false)) + break; + CloseHandle(rmutex); + } + bool ok = true; + if (idx >= MAX_READERS) { + qWarning("QtLockedFile::lock(): too many readers"); + rmutex = 0; + ok = false; + } + else if (!rmutex) { + rmutex = getMutexHandle(idx, true); + if (!rmutex || !waitMutex(rmutex, false)) + ok = false; + } + if (!ok && rmutex) { + CloseHandle(rmutex); + rmutex = 0; + } + ReleaseMutex(wmutex); + if (!ok) + return false; + } + else { + Q_ASSERT(rmutexes.isEmpty()); + for (int i = 0; i < MAX_READERS; i++) { + Qt::HANDLE mutex = getMutexHandle(i, false); + if (mutex) + rmutexes.append(mutex); + } + if (rmutexes.size()) { + DWORD res = WaitForMultipleObjects(rmutexes.size(), rmutexes.constData(), + TRUE, block ? INFINITE : 0); + if (res != WAIT_OBJECT_0 && res != WAIT_ABANDONED) { + if (res != WAIT_TIMEOUT) + qErrnoWarning("QtLockedFile::lock(): WaitForMultipleObjects failed"); + m_lock_mode = WriteLock; // trick unlock() to clean up - semiyucky + unlock(); + return false; + } + } + } + + m_lock_mode = mode; + return true; +} + +bool LockedFile::unlock() +{ + if (!isOpen()) { + qWarning("QtLockedFile::unlock(): file is not opened"); + return false; + } + + if (!isLocked()) + return true; + + if (m_lock_mode == ReadLock) { + ReleaseMutex(rmutex); + CloseHandle(rmutex); + rmutex = 0; + } + else { + foreach(Qt::HANDLE mutex, rmutexes) { + ReleaseMutex(mutex); + CloseHandle(mutex); + } + rmutexes.clear(); + ReleaseMutex(wmutex); + } + + m_lock_mode = LockedFile::NoLock; + return true; +} + +LockedFile::~LockedFile() +{ + if (isOpen()) + unlock(); + if (wmutex) + CloseHandle(wmutex); +} diff --git a/libraries/README.md b/libraries/README.md index 809acb16..9d91d098 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -142,4 +142,11 @@ Available either under LGPL version 2.1 or later. ## xz-embedded Tiny implementation of LZMA2 de/compression. This format is only used by Forge to save bandwidth. -Public domain. \ No newline at end of file +Public domain. + +## LocalPeer +Library for making only one instance of the application run at all times. + +BSD licensed, derived from [QtSingleApplication](https://github.com/qtproject/qt-solutions/tree/master/qtsingleapplication). + +Changes are made to make the code more generic and useful in less usual conditions. -- cgit v1.2.3