diff --git a/shell/core.cpp b/shell/core.cpp index 206d48d184..4b274b5a57 100644 --- a/shell/core.cpp +++ b/shell/core.cpp @@ -1,547 +1,530 @@ /*************************************************************************** * Copyright 2007 Alexander Dymo * * Copyright 2007 Kris Wong * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "core.h" #include "core_p.h" #include #include #include #include #include #include #include #include #include #include #include #include "shellextension.h" #include "mainwindow.h" #include "sessioncontroller.h" #include "uicontroller.h" #include "plugincontroller.h" #include "projectcontroller.h" #include "partcontroller.h" #include "languagecontroller.h" #include "documentcontroller.h" #include "runcontroller.h" #include "session.h" #include "documentationcontroller.h" #include "sourceformattercontroller.h" #include "progressmanager.h" #include "selectioncontroller.h" #include "debugcontroller.h" #include "kdevplatformversion.h" #include "workingsetcontroller.h" #include #include #include #include volatile std::sig_atomic_t handlingSignal = 0; void shutdownGracefully(int sig) { if ( !handlingSignal ) { handlingSignal = 1; qDebug() << "signal " << sig << " received, shutting down gracefully"; QCoreApplication::instance()->quit(); } else { // re-raise signal with default handler and trigger program termination std::signal(sig, SIG_DFL); std::raise(sig); } } void installSignalHandler() { #ifdef SIGHUP std::signal(SIGHUP, shutdownGracefully); #endif #ifdef SIGINT std::signal(SIGINT, shutdownGracefully); #endif #ifdef SIGTERM std::signal(SIGTERM, shutdownGracefully); #endif } namespace KDevelop { Core *Core::m_self = 0; KAboutData aboutData() { KAboutData aboutData( "kdevplatform", "kdevplatform", ki18n("KDevelop Platform"), KDEVPLATFORM_VERSION_STR, ki18n("Development Platform for IDE-like Applications"), KAboutData::License_LGPL_V2, ki18n( "Copyright 2004-2009, The KDevelop developers" ), KLocalizedString(), "http://www.kdevelop.org" ); aboutData.addAuthor( ki18n("Andreas Pakulat"), ki18n( "Architecture, VCS Support, Project Management Support, QMake Projectmanager" ), "apaku@gmx.de" ); aboutData.addAuthor( ki18n("Alexander Dymo"), ki18n( "Architecture, Sublime UI, Ruby support" ), "adymo@kdevelop.org" ); aboutData.addAuthor( ki18n("David Nolden"), ki18n( "Definition-Use Chain, C++ Support" ), "david.nolden.kdevelop@art-master.de" ); aboutData.addAuthor( ki18n("Aleix Pol Gonzalez"), ki18n( "Co-Maintainer, CMake Support, Run Support, Kross Support" ), "aleixpol@kde.org" ); aboutData.addAuthor( ki18n("Vladimir Prus"), ki18n( "GDB integration" ), "ghost@cs.msu.su" ); aboutData.addAuthor( ki18n("Hamish Rodda"), ki18n( "Text editor integration, definition-use chain" ), "rodda@kde.org" ); aboutData.addCredit( ki18n("Matt Rogers"), KLocalizedString(), "mattr@kde.org"); aboutData.addCredit( ki18n("Cédric Pasteur"), ki18n("astyle and indent support"), "cedric.pasteur@free.fr" ); aboutData.addCredit( ki18n("Evgeniy Ivanov"), ki18n("Distributed VCS, Git, Mercurial"), "powerfox@kde.ru" ); //Veritas is outside in playground currently. //aboutData.addCredit( ki18n("Manuel Breugelmanns"), ki18n( "Veritas, QTest integraton"), "mbr.nxi@gmail.com" ); aboutData.addCredit( ki18n("Robert Gruber") , ki18n( "SnippetPart, debugger and usability patches" ), "rgruber@users.sourceforge.net" ); aboutData.addCredit( ki18n("Dukju Ahn"), ki18n( "Subversion plugin, Custom Make Manager, Overall improvements" ), "dukjuahn@gmail.com" ); aboutData.addAuthor( ki18n("Niko Sams"), ki18n( "GDB integration, Webdevelopment Plugins" ), "niko.sams@gmail.com" ); aboutData.addAuthor( ki18n("Milian Wolff"), ki18n( "Co-Maintainer, Generic manager, Webdevelopment Plugins, Snippets, Performance" ), "mail@milianw.de" ); return aboutData; } CorePrivate::CorePrivate(Core *core): m_componentData( aboutData() ), m_core(core), m_cleanedUp(false), m_shuttingDown(false) { } bool CorePrivate::initialize(Core::Setup mode, QString session ) { m_mode=mode; if( !sessionController ) { sessionController = new SessionController(m_core); } if( !workingSetController && !(mode & Core::NoUi) ) { workingSetController = new WorkingSetController(m_core); } kDebug() << "Creating ui controller"; if( !uiController ) { uiController = new UiController(m_core); } kDebug() << "Creating plugin controller"; if( !pluginController ) { pluginController = new PluginController(m_core); } if( !partController && !(mode & Core::NoUi)) { partController = new PartController(m_core, uiController.data()->defaultMainWindow()); { // check features of kate and report to user if it does not fit KTextEditor::Document* doc = partController.data()->createTextPart(); if ( !qobject_cast< KTextEditor::MovingInterface* >(doc) ) { KMessageBox::error(QApplication::activeWindow(), i18n("The installed Kate version does not support the MovingInterface which is crucial for " "KDevelop starting from version 4.2.\n\n" "To use KDevelop with KDE SC prior to 4.6, where the SmartInterface is used instead " "of the MovingInterface, you need KDevelop 4.1 or lower.")); delete doc; return false; } delete doc; } } if( !projectController ) { projectController = new ProjectController(m_core); } if( !documentController ) { documentController = new DocumentController(m_core); } if( !languageController ) { // Must be initialized after documentController, because the background parser depends // on the document controller. languageController = new LanguageController(m_core); } if( !runController ) { runController = new RunController(m_core); } if( !sourceFormatterController ) { sourceFormatterController = new SourceFormatterController(m_core); } if ( !progressController) { progressController = new ProgressManager(); } if( !selectionController ) { selectionController = new SelectionController(m_core); } if( !documentationController && !(mode & Core::NoUi) ) { documentationController = new DocumentationController(m_core); } if( !debugController ) { debugController = new DebugController(m_core); } kDebug() << "initializing ui controller"; - - if( !session.isEmpty() && !SessionController::tryLockSession(session) && !(mode & Core::NoUi) ) - { - QString errmsg = i18n("The session %1 is already active in another running instance. Choose another session or close the running instance.", session ); - session = SessionController::showSessionChooserDialog(errmsg); - if(session.isEmpty()) - return false; - } - - sessionController.data()->initialize( session ); - if(!sessionController.data()->lockSession()) - { - QString errmsg = i18n("Failed to lock the session %1, probably it is already active in another running instance", session ); - if( mode & Core::NoUi ) { - QTextStream qerr(stderr); - qerr << endl << errmsg << endl; - } else { - KMessageBox::error(0, errmsg); - } + sessionController.data()->initialize( session ); + if( !sessionController.data()->activeSession() ) { return false; } // TODO: Is this early enough, or should we put the loading of the session into // the controller construct DUChain::initialize(); if(!(mode & Core::NoUi)) uiController.data()->initialize(); languageController.data()->initialize(); projectController.data()->initialize(); documentController.data()->initialize(); /* This is somewhat messy. We want to load the areas before loading the plugins, so that when each plugin is loaded we know if an area wants some of the tool view from that plugin. OTOH, loading of areas creates documents, and some documents might require that a plugin is already loaded. Probably, the best approach would be to plugins to just add tool views to a list of available tool view, and then grab those tool views when loading an area. */ kDebug() << "loading session plugins"; pluginController.data()->initialize(); if(!(mode & Core::NoUi)) { workingSetController.data()->initialize(); /* Need to do this after everything else is loaded. It's too hard to restore position of views, and toolbars, and whatever that are not created yet. */ uiController.data()->loadAllAreas(KGlobal::config()); uiController.data()->defaultMainWindow()->show(); } runController.data()->initialize(); sourceFormatterController.data()->initialize(); selectionController.data()->initialize(); if (documentationController) { documentationController.data()->initialize(); } debugController.data()->initialize(); installSignalHandler(); return true; } CorePrivate::~CorePrivate() { delete selectionController.data(); delete projectController.data(); delete languageController.data(); delete pluginController.data(); delete uiController.data(); delete partController.data(); delete documentController.data(); delete runController.data(); delete sessionController.data(); delete sourceFormatterController.data(); delete documentationController.data(); delete debugController.data(); delete workingSetController.data(); selectionController.clear(); projectController.clear(); languageController.clear(); pluginController.clear(); uiController.clear(); partController.clear(); documentController.clear(); runController.clear(); sessionController.clear(); sourceFormatterController.clear(); documentationController.clear(); debugController.clear(); workingSetController.clear(); } bool Core::initialize(KSplashScreen* splash, Setup mode, const QString& session ) { if (m_self) return true; m_self = new Core(); bool ret = m_self->d->initialize(mode, session); if( splash ) { QTimer::singleShot( 200, splash, SLOT(deleteLater()) ); } if(ret) emit m_self->initializationCompleted(); return ret; } Core *KDevelop::Core::self() { return m_self; } Core::Core(QObject *parent) : ICore(parent) { d = new CorePrivate(this); connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(shutdown())); } Core::Core(CorePrivate* dd, QObject* parent) : ICore(parent), d(dd) { connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(shutdown())); } Core::~Core() { kDebug() ; //Cleanup already called before mass destruction of GUI delete d; m_self = 0; } Core::Setup Core::setupFlags() const { return d->m_mode; } void Core::shutdown() { if (!d->m_shuttingDown) { cleanup(); deleteLater(); } } bool Core::shuttingDown() const { return d->m_shuttingDown; } void Core::cleanup() { d->m_shuttingDown = true; emit aboutToShutdown(); if (!d->m_cleanedUp) { d->debugController.data()->cleanup(); d->selectionController.data()->cleanup(); // Save the layout of the ui here, so run it first d->uiController.data()->cleanup(); if (d->workingSetController) d->workingSetController.data()->cleanup(); /* Must be called before projectController.data()->cleanup(). */ // Closes all documents (discards, as already saved if the user wished earlier) d->documentController.data()->cleanup(); d->runController.data()->cleanup(); d->projectController.data()->cleanup(); d->sourceFormatterController.data()->cleanup(); d->pluginController.data()->cleanup(); d->sessionController.data()->cleanup(); //Disable the functionality of the language controller d->languageController.data()->cleanup(); } d->m_cleanedUp = true; } KComponentData Core::componentData() const { return d->m_componentData; } IUiController *Core::uiController() { return d->uiController.data(); } ISession* Core::activeSession() { return sessionController()->activeSession(); } SessionController *Core::sessionController() { return d->sessionController.data(); } UiController *Core::uiControllerInternal() { return d->uiController.data(); } IPluginController *Core::pluginController() { return d->pluginController.data(); } PluginController *Core::pluginControllerInternal() { return d->pluginController.data(); } IProjectController *Core::projectController() { return d->projectController.data(); } ProjectController *Core::projectControllerInternal() { return d->projectController.data(); } IPartController *Core::partController() { return d->partController.data(); } PartController *Core::partControllerInternal() { return d->partController.data(); } ILanguageController *Core::languageController() { return d->languageController.data(); } LanguageController *Core::languageControllerInternal() { return d->languageController.data(); } IDocumentController *Core::documentController() { return d->documentController.data(); } DocumentController *Core::documentControllerInternal() { return d->documentController.data(); } IRunController *Core::runController() { return d->runController.data(); } RunController *Core::runControllerInternal() { return d->runController.data(); } ISourceFormatterController* Core::sourceFormatterController() { return d->sourceFormatterController.data(); } SourceFormatterController* Core::sourceFormatterControllerInternal() { return d->sourceFormatterController.data(); } ProgressManager *Core::progressController() { return d->progressController.data(); } ISelectionController* Core::selectionController() { return d->selectionController.data(); } IDocumentationController* Core::documentationController() { return d->documentationController.data(); } DocumentationController* Core::documentationControllerInternal() { return d->documentationController.data(); } IDebugController* Core::debugController() { return d->debugController.data(); } DebugController* Core::debugControllerInternal() { return d->debugController.data(); } WorkingSetController* Core::workingSetControllerInternal() { return d->workingSetController.data(); } QString Core::version() { return KDEVPLATFORM_VERSION_STR; } } #include "core.moc" diff --git a/shell/sessioncontroller.cpp b/shell/sessioncontroller.cpp index c9fac677f9..10e04329ea 100644 --- a/shell/sessioncontroller.cpp +++ b/shell/sessioncontroller.cpp @@ -1,1078 +1,1277 @@ /* This file is part of KDevelop Copyright 2008 Andreas Pakulat Copyright 2010 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "sessioncontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "session.h" #include "core.h" #include "uicontroller.h" #include "sessiondialog.h" #include "shellextension.h" #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include +#include #include #if KDE_IS_VERSION(4,5,60) #define HAVE_RECOVERY_INTERFACE #include #endif const int recoveryStorageInterval = 10; ///@todo Make this configurable namespace KDevelop { namespace { int argc = 0; char** argv = 0; }; void SessionController::setArguments(int _argc, char** _argv) { argc = _argc; argv = _argv; } static QStringList standardArguments() { QStringList ret; for(int a = 0; a < argc; ++a) { QString arg = QString::fromLocal8Bit(argv[a]); kWarning() << "ARG:" << "" + arg + ""; /* if(arg.startsWith("--graphicssystem=") || arg.startsWith("--style=")) { ret << arg; }else */ if(arg.startsWith("-graphicssystem") || arg.startsWith("-style")) { ret << '-' + arg; if(a+1 < argc) ret << QString::fromLocal8Bit(argv[a+1]); } } kWarning() << "ARGUMENTS: " << ret << "from" << argc; return ret; } class SessionControllerPrivate : public QObject { Q_OBJECT public: SessionControllerPrivate( SessionController* s ) : q(s) , activeSession(0) , grp(0) , recoveryDirectoryIsOwn(false) { recoveryTimer.setInterval(recoveryStorageInterval * 1000); connect(&recoveryTimer, SIGNAL(timeout()), SLOT(recoveryStorageTimeout())); // Try the recovery only after the initialization has finished connect(ICore::self(), SIGNAL(initializationCompleted()), SLOT(lateInitialization()), Qt::QueuedConnection); recoveryTimer.setSingleShot(false); recoveryTimer.start(); } ~SessionControllerPrivate() { if (activeSession) { // when session was active, we deleted the folder already // in that case activeSession = 0 clearRecoveryDirectory(); } } Session* findSessionForName( const QString& name ) const { foreach( Session* s, sessionActions.keys() ) { if( s->name() == name ) return s; } return 0; } Session* findSessionForId(QString idString) { QUuid id(idString); foreach( Session* s, sessionActions.keys() ) { if( s->id() == id) return s; } return 0; } void newSession() { qsrand(QDateTime::currentDateTime().toTime_t()); Session* session = new Session( QUuid::createUuid() ); KProcess::startDetached(ShellExtension::getInstance()->binaryPath(), QStringList() << "-s" << session->id().toString() << standardArguments()); delete session; #if 0 //Terminate this instance of kdevelop if the user agrees foreach(Sublime::MainWindow* window, Core::self()->uiController()->controller()->mainWindows()) window->close(); #endif } void configureSessions() { SessionDialog dlg(ICore::self()->uiController()-> activeMainWindow()); dlg.exec(); } void deleteSession() { int choice = KMessageBox::warningContinueCancel(Core::self()->uiController()->activeMainWindow(), i18n("The current session and all contained settings will be deleted. The projects will stay unaffected. Do you really want to continue?")); if(choice == KMessageBox::Continue) { static_cast(q->activeSession())->deleteFromDisk(); q->emitQuitSession(); } } void renameSession() { KDialog dialog; dialog.setWindowTitle(i18n("Rename Session")); QGroupBox box; QHBoxLayout layout(&box); box.setTitle(i18n("New Session Name")); QLineEdit edit; layout.addWidget(&edit); dialog.setButtons(KDialog::Ok | KDialog::Cancel); edit.setText(q->activeSession()->name()); dialog.setMainWidget(&box); edit.setFocus(); if(dialog.exec() == QDialog::Accepted) { static_cast(q->activeSession())->setName(edit.text()); } } bool loadSessionExternally( Session* s ) { Q_ASSERT( s ); - if( !SessionController::tryLockSession(s->id().toString()) ) - { - KMessageBox::error(ICore::self()->uiController()->activeMainWindow(), i18n("The selected session is already active in another running instance")); - return false; - }else{ - KProcess::startDetached(ShellExtension::getInstance()->binaryPath(), QStringList() << "-s" << s->id().toString() << standardArguments()); - return true; - } + KProcess::startDetached(ShellExtension::getInstance()->binaryPath(), QStringList() << "-s" << s->id().toString() << standardArguments()); + return true; } void activateSession( Session* s ) { Q_ASSERT( s ); + activeSession = s; + if( !lockSession() ) { + activeSession = 0; + return; + } + KConfigGroup grp = KGlobal::config()->group( SessionController::cfgSessionGroup() ); grp.writeEntry( SessionController::cfgActiveSessionEntry(), s->id().toString() ); grp.sync(); - activeSession = s; if (Core::self()->setupFlags() & Core::NoUi) return; QHash::iterator it = sessionActions.find(s); Q_ASSERT( it != sessionActions.end() ); (*it)->setCheckable(true); (*it)->setChecked(true); for(it = sessionActions.begin(); it != sessionActions.end(); ++it) { if(it.key() != s) (*it)->setCheckable(false); } } void loadSessionFromAction( QAction* a ) { foreach( Session* s, sessionActions.keys() ) { if( s->id() == QUuid( a->data().toString() ) && s != activeSession ) { loadSessionExternally( s ); break; } } } void addSession( Session* s ) { if (Core::self()->setupFlags() & Core::NoUi) { sessionActions[s] = 0; return; } KAction* a = new KAction( grp ); a->setText( s->description() ); a->setCheckable( false ); a->setData( s->id().toString() ); sessionActions[s] = a; q->actionCollection()->addAction( "session_"+s->id().toString(), a ); q->unplugActionList( "available_sessions" ); q->plugActionList( "available_sessions", grp->actions() ); connect(s, SIGNAL(nameChanged(QString,QString)), SLOT(nameChanged())); } SessionController* q; QHash sessionActions; ISession* activeSession; QActionGroup* grp; KLockFile::Ptr sessionLock; + SessionController::LockSessionState sessionLockState; // Whether this process owns the recovery directory bool recoveryDirectoryIsOwn; QTimer recoveryTimer; QMap currentRecoveryFiles; + static QString sessionBaseDirectory() + { + return KGlobal::mainComponent().dirs()->saveLocation( "data", KGlobal::mainComponent().componentName() + "/sessions/", true ); + } + + static QString sessionDirectory( const QString& id ) + { + return sessionBaseDirectory() + id; + } + + static QString lockFileForSession( const QString& id ) + { + return sessionDirectory( id ) + "/lock"; + } + + static QString DBusServiceNameForSession( const QString& id ) + { + // We remove starting "{" and ending "}" from the string UUID representation + // as D-Bus apparently doesn't allow them in service names + return QString( "org.kdevelop.kdevplatform-lock-" ) + QString( id ).mid( 1, id.size() - 2 ); + } + + QString ownDBusServiceName() + { + Q_ASSERT(activeSession); + return DBusServiceNameForSession( activeSession->id() ); + } QString ownSessionDirectory() const { Q_ASSERT(activeSession); - return SessionController::sessionDirectory() + '/' + activeSession->id().toString(); + return sessionDirectory( activeSession->id().toString() ); } - + + QString ownLockFile() + { + Q_ASSERT(activeSession); + return lockFileForSession( activeSession->id().toString() ); + } + void clearRecoveryDirectory() { removeDirectory(ownSessionDirectory() + "/recovery"); } + bool lockSession() + { + sessionLockState = SessionController::tryLockSession( activeSession->id(), true ); + return sessionLockState; + } + public slots: void documentSavedOrClosed( KDevelop::IDocument* document ) { if(currentRecoveryFiles.contains(document->url())) { kDebug() << "deleting recovery-info for" << document->url(); foreach(const QString& recoveryFileName, currentRecoveryFiles[document->url()]) { bool result = QFile::remove(recoveryFileName); kDebug() << "deleted" << recoveryFileName << result; } currentRecoveryFiles.remove(document->url()); } } private slots: void lateInitialization() { performRecovery(); connect(Core::self()->documentController(), SIGNAL(documentSaved(KDevelop::IDocument*)), SLOT(documentSavedOrClosed(KDevelop::IDocument*))); connect(Core::self()->documentController(), SIGNAL(documentClosed(KDevelop::IDocument*)), SLOT(documentSavedOrClosed(KDevelop::IDocument*))); } void performRecovery() { kDebug() << "Checking recovery"; QDir recoveryDir(ownSessionDirectory() + "/recovery"); if(recoveryDir.exists()) { kDebug() << "Have recovery directory, starting recovery"; QFile dateFile(recoveryDir.path() + "/date"); dateFile.open(QIODevice::ReadOnly); QString date = QString::fromUtf8(dateFile.readAll()); QDir recoverySubDir(recoveryDir.path() + "/current"); if(!recoverySubDir.exists()) recoverySubDir = QDir(recoveryDir.path() + "/backup"); if(recoverySubDir.exists()) { kWarning() << "Starting recovery from " << recoverySubDir.absolutePath(); QStringList urlList; for(uint num = 0; ; ++num) { QFile urlFile(recoverySubDir.path() + QString("/%1_url").arg(num)); if(!urlFile.exists()) break; urlFile.open(QIODevice::ReadOnly); KUrl originalFile(QString::fromUtf8(urlFile.readAll())); urlList << originalFile.pathOrUrl(); } if(!urlList.isEmpty()) { //Either recover, or delete the recovery directory ///TODO: user proper runtime locale for date, it might be different /// from what was used when the recovery file was saved KGuiItem recover = KStandardGuiItem::cont(); recover.setIcon(KIcon("edit-redo")); recover.setText(i18n("Recover")); KGuiItem discard = KStandardGuiItem::discard(); int choice = KMessageBox::warningContinueCancelList(qApp->activeWindow(), i18nc("%1: date of the last snapshot", "The session crashed the last time it was used. " "The following modified files can be recovered from a backup from %1.", date), urlList, i18n("Crash Recovery"), recover, discard ); if(choice == KMessageBox::Continue) { #if 0 { //Put the recovered documents into the "Review" area, and clear the working set ICore::self()->uiController()->switchToArea("review", KDevelop::IUiController::ThisWindow); Sublime::MainWindow* window = static_cast(ICore::self()->uiController()->activeMainWindow()); window->area()->setWorkingSet("recover"); window->area()->clearViews(); } #endif //Recover the files for(uint num = 0; ; ++num) { QFile urlFile(recoverySubDir.path() + QString("/%1_url").arg(num)); if(!urlFile.exists()) break; urlFile.open(QIODevice::ReadOnly); KUrl originalFile(QString::fromUtf8(urlFile.readAll())); QFile f(recoverySubDir.path() + '/' + QString("/%1_text").arg(num)); f.open(QIODevice::ReadOnly); QString text = QString::fromUtf8(f.readAll()); if(text.isEmpty()) { KMessageBox::error(ICore::self()->uiController()->activeMainWindow(), i18n("Could not recover %1, the recovery file is empty", originalFile.pathOrUrl()), i18n("Recovery")); continue; } kDebug() << "opening" << originalFile << "for recovery"; KDevelop::IDocument* doc = ICore::self()->documentController()->openDocument(originalFile); if(!doc || !doc->textDocument()) { kWarning() << "The document " << originalFile.prettyUrl() << " could not be opened as a text-document, creating a new document with the recovered contents"; doc = ICore::self()->documentController()->openDocumentFromText(text); }else{ #ifdef HAVE_RECOVERY_INTERFACE KTextEditor::RecoveryInterface* recovery = qobject_cast(doc->textDocument()); if(recovery && recovery->isDataRecoveryAvailable()) // Use the recovery from the kate swap-file if possible recovery->recoverData(); else // Use a simple recovery through "replace text" doc->textDocument()->setText(text); #else // Use a simple recovery through "replace text" doc->textDocument()->setText(text); #endif } } } } } } recoveryDirectoryIsOwn = true; } void nameChanged() { Q_ASSERT(qobject_cast(sender())); Session* s = static_cast(sender()); sessionActions[s]->setText( s->description() ); } void recoveryStorageTimeout() { if(!recoveryDirectoryIsOwn) return; currentRecoveryFiles.clear(); QDir recoveryDir(ownSessionDirectory() + "/recovery"); if(!recoveryDir.exists()) { // Try "taking" the recovery directory QDir sessionDir(ownSessionDirectory()); if(!sessionDir.mkdir("recovery")) return; } if (recoveryDir.exists("backup")) { // Clear the old backup recovery directory, as we will create a new one if (!removeDirectory(recoveryDir.absoluteFilePath("backup"))) { kWarning() << "RECOVERY ERROR: Removing the old recovery backup directory failed in " << recoveryDir; return; } } //Make the current recovery dir the backup dir, so we always have a recovery available //This may fail, because "current" might be nonexistent recoveryDir.rename("current", "backup"); { recoveryDir.mkdir("current_incomplete"); QDir recoveryCurrentDir(recoveryDir.path() + "/current_incomplete"); uint num = 0; foreach(KDevelop::IDocument* document, ICore::self()->documentController()->openDocuments()) { if(document->state() == IDocument::Modified || document->state() == IDocument::DirtyAndModified) { //This document was modified, create a recovery-backup if(document->textDocument()) { //Currently we can only back-up text documents QString text = document->textDocument()->text(); if(!text.isEmpty()) { QString urlFilePath = recoveryCurrentDir.path() + QString("/%1_url").arg(num); QFile urlFile(urlFilePath); urlFile.open(QIODevice::WriteOnly); urlFile.write(document->url().pathOrUrl().toUtf8()); urlFile.close(); QString textFilePath = recoveryCurrentDir.path() + '/' + QString("/%1_text").arg(num); QFile f(textFilePath); f.open(QIODevice::WriteOnly); f.write(text.toUtf8()); f.close(); currentRecoveryFiles[document->url()] = QStringList() << (recoveryDir.path() + "/current" + QString("/%1_url").arg(num)) << (recoveryDir.path() + "/current" + QString("/%1_text").arg(num)); if(urlFile.error() != QFile::NoError || f.error() != QFile::NoError) { kWarning() << "RECOVERY ERROR: Failed to write recovery for" << document->url() << "to" << textFilePath; KMessageBox::error(ICore::self()->uiController()->activeMainWindow(), i18n("Failed to write recovery copies to %1. Please make sure that your home directory is writable and not full. This application requires available space in the home directory to run stable. You may experience application crashes until you free up some space.", recoveryCurrentDir.path()), i18n("Recovery Error")); return; } ++num; } } } } } recoveryDir.rename("current_incomplete", "current"); { //Write down the date of the recovery QFile dateFile(recoveryDir.path() + "/date"); dateFile.open(QIODevice::WriteOnly); dateFile.write(QDateTime::currentDateTime().toString(Qt::DefaultLocaleShortDate).toUtf8()); } } }; -bool SessionController::lockSession() -{ - d->sessionLock = new KLockFile(d->ownSessionDirectory() + "/lock"); - KLockFile::LockResult result = d->sessionLock->lock(KLockFile::NoBlockFlag | KLockFile::ForceFlag); - - return result == KLockFile::LockOK; -} void SessionController::updateSessionDescriptions() { for(QHash< Session*, QAction* >::iterator it = d->sessionActions.begin(); it != d->sessionActions.end(); ++it) { it.key()->updateDescription(); if (*it) (*it)->setText(it.key()->description()); } } SessionController::SessionController( QObject *parent ) : QObject( parent ), d(new SessionControllerPrivate(this)) { setObjectName("SessionController"); setComponentData(KComponentData("kdevsession")); setXMLFile("kdevsessionui.rc"); QDBusConnection::sessionBus().registerObject( "/kdevelop/SessionController", this, QDBusConnection::ExportScriptableSlots ); if (Core::self()->setupFlags() & Core::NoUi) return; KAction* action = actionCollection()->addAction( "new_session", this, SLOT(newSession()) ); action->setText( i18nc("@action:inmenu", "Start New Session") ); action->setToolTip( i18nc("@info:tooltip", "Start a new KDevelop instance with an empty session") ); action->setIcon(KIcon("window-new")); action = actionCollection()->addAction( "rename_session", this, SLOT(renameSession()) ); action->setText( i18n("Rename Session...") ); action->setIcon(KIcon("edit-rename")); action = actionCollection()->addAction( "delete_session", this, SLOT(deleteSession()) ); action->setText( i18n("Delete Session...") ); action->setIcon(KIcon("edit-delete")); action = actionCollection()->addAction( "quit", this, SIGNAL(quitSession()) ); action->setText( i18n("Quit") ); action->setShortcut(Qt::CTRL | Qt::Key_Q); action->setIcon(KIcon("application-exit")); #if 0 action = actionCollection()->addAction( "configure_sessions", this, SLOT(configureSessions()) ); action->setText( i18n("Configure Sessions...") ); action->setToolTip( i18n("Create/Delete/Activate Sessions") ); action->setWhatsThis( i18n( "Configure Sessions

Shows a dialog to Create/Delete Sessions and set a new active session.

" ) ); #endif d->grp = new QActionGroup( this ); connect( d->grp, SIGNAL(triggered(QAction*)), this, SLOT(loadSessionFromAction(QAction*)) ); } SessionController::~SessionController() { delete d; } void SessionController::startNewSession() { d->newSession(); } void SessionController::cleanup() { d->recoveryTimer.stop(); ISession* active = d->activeSession; d->activeSession = 0; if (active->isTemporary()) { deleteSession(active->name()); } qDeleteAll(d->sessionActions); d->sessionActions.clear(); } void SessionController::initialize( const QString& session ) { QDir sessiondir( SessionController::sessionDirectory() ); foreach( const QString& s, sessiondir.entryList( QDir::AllDirs ) ) { QUuid id( s ); if( id.isNull() ) continue; // Only create sessions for directories that represent proper uuid's Session* ses = new Session( id, this ); //Delete sessions that have no name and are empty if( ses->containedProjects().isEmpty() && ses->name().isEmpty() && (session.isEmpty() || (ses->id().toString() != session && ses->name() != session)) ) { ///@todo Think about when we can do this. Another instance might still be using this session. // session->deleteFromDisk(); delete ses; }else{ d->addSession( ses ); } } loadDefaultSession( session ); connect(Core::self()->projectController(), SIGNAL(projectClosed(KDevelop::IProject*)), SLOT(updateSessionDescriptions())); connect(Core::self()->projectController(), SIGNAL(projectOpened(KDevelop::IProject*)), SLOT(updateSessionDescriptions())); updateSessionDescriptions(); } ISession* SessionController::activeSession() const { return d->activeSession; } void SessionController::loadSession( const QString& nameOrId ) { d->loadSessionExternally( session( nameOrId ) ); } QList SessionController::sessionNames() const { QStringList l; foreach( const Session* s, d->sessionActions.keys() ) { l << s->name(); } return l; } QList< const KDevelop::Session* > SessionController::sessions() const { QList< const KDevelop::Session* > ret; foreach( const Session* s, d->sessionActions.keys() ) { ret << s; } return ret; } Session* SessionController::createSession( const QString& name ) { Session* s; if(name.startsWith('{')) { s = new Session( QUuid(name) ); }else{ qsrand(QDateTime::currentDateTime().toTime_t()); s = new Session( QUuid::createUuid() ); s->setName( name ); } d->addSession( s ); return s; } void SessionController::deleteSession( const QString& nameOrId ) { Session* s = session(nameOrId); Q_ASSERT( s != d->activeSession ) ; QHash::iterator it = d->sessionActions.find(s); Q_ASSERT( it != d->sessionActions.end() ); unplugActionList( "available_sessions" ); actionCollection()->removeAction(*it); if (d->grp) { // happens in unit tests d->grp->removeAction(*it); plugActionList( "available_sessions", d->grp->actions() ); } s->deleteFromDisk(); emit sessionDeleted( s->name() ); d->sessionActions.remove(s); s->deleteLater(); } void SessionController::loadDefaultSession( const QString& session ) { QString load = session; if (load.isEmpty()) { KConfigGroup grp = KGlobal::config()->group( cfgSessionGroup() ); load = grp.readEntry( cfgActiveSessionEntry(), "default" ); } - Session* s = this->session( load ); - if( !s ) + // Iteratively try to load the session, asking user what to do in case of failure + // If showForceOpenDialog() returns empty string, stop trying + Session* s; + do { - s = createSession( load ); - } - d->activateSession( s ); + s = this->session( load ); + if( !s ) { + s = createSession( load ); + } + d->activateSession( s ); + if( activeSession() ) + break; + load = handleLockedSession( s->name(), s->id().toString(), d->sessionLockState ); + } while( !load.isEmpty() ); } Session* SessionController::session( const QString& nameOrId ) const { Session* ret = d->findSessionForName( nameOrId ); if(ret) return ret; return d->findSessionForId( nameOrId ); } QString SessionController::cloneSession( const QString& nameOrid ) { Session* origSession = session( nameOrid ); qsrand(QDateTime::currentDateTime().toTime_t()); QUuid id = QUuid::createUuid(); - KIO::NetAccess::dircopy( KUrl( sessionDirectory() + '/' + origSession->id().toString() ), - KUrl( sessionDirectory() + '/' + id.toString() ), + KIO::NetAccess::dircopy( SessionControllerPrivate::sessionDirectory( origSession->id().toString() ), + SessionControllerPrivate::sessionDirectory( id.toString() ), Core::self()->uiController()->activeMainWindow() ); Session* newSession = new Session( id ); newSession->setName( i18n( "Copy of %1", origSession->name() ) ); d->addSession(newSession); return newSession->name(); } void SessionController::plugActions() { unplugActionList( "available_sessions" ); plugActionList( "available_sessions", d->grp->actions() ); } QString SessionController::cfgSessionGroup() { return "Sessions"; } QString SessionController::cfgActiveSessionEntry() { return "Active Session ID"; } QList< SessionInfo > SessionController::availableSessionInfo() { QList< SessionInfo > available; QDir sessiondir( SessionController::sessionDirectory() ); foreach( const QString& s, sessiondir.entryList( QDir::AllDirs ) ) { QUuid id( s ); if( id.isNull() ) continue; // TODO: Refactor the code here and in session.cpp so its shared SessionInfo si; si.uuid = id; KSharedConfig::Ptr config = KSharedConfig::openConfig( sessiondir.absolutePath() + '/' + s +"/sessionrc" ); QString desc = config->group( "" ).readEntry( "SessionName", "" ); si.name = desc; si.projects = config->group( "General Options" ).readEntry( "Open Projects", QStringList() ); QString prettyContents = config->group("").readEntry( "SessionPrettyContents", "" ); if(!prettyContents.isEmpty()) { if(!desc.isEmpty()) desc += ": "; desc += prettyContents; } si.description = desc; available << si; } return available; } QString SessionController::sessionDirectory() { - return KGlobal::mainComponent().dirs()->saveLocation( "data", KGlobal::mainComponent().componentName()+"/sessions", true ); + return SessionControllerPrivate::sessionBaseDirectory(); } -QString SessionController::sessionDir() -{ - return sessionDirectory() + '/' + activeSession()->id().toString(); -} - -SessionController::LockSessionState SessionController::tryLockSession(QString id) +SessionController::LockSessionState SessionController::tryLockSession(QString id, bool doLocking) { + /* + * We've got two locking mechanisms here: D-Bus unique service name (based on the session id) + * and a plain lockfile (KLockFile). + * The latter is required to get the appname/pid of the locking instance + * in case if it's stale/hanging/crashed (to make user know which PID he needs to kill). + * D-Bus mechanism is the primary one. + * + * Since there is a kind of "logic tree", the code is a bit hard. + */ LockSessionState ret; - - ret.lockFile = sessionDirectory() + '/' + id + "/lock"; + ret.sessionId = id; - if(!QFileInfo(ret.lockFile).exists()) - { - // Maybe the session doesn't exist yet - ret.success = true; - return ret; + QString service = SessionControllerPrivate::DBusServiceNameForSession( id ); + QDBusConnection connection = QDBusConnection::sessionBus(); + QDBusInterface rootInterface( service, "/", QString(), connection ); + ret.DBusService = service; + + ret.lockFilename = SessionControllerPrivate::lockFileForSession( id ); + ret.lockFile = new KLockFile( ret.lockFilename ); + + bool canLockDBus = !rootInterface.isValid(); + bool lockedDBus = false; + + // Lock D-Bus if we can and we need to + if( doLocking && canLockDBus ) { + lockedDBus = connection.registerService( service ); } - - KLockFile::Ptr lock(new KLockFile(ret.lockFile)); - ret.success = lock->lock(KLockFile::NoBlockFlag | KLockFile::ForceFlag) == KLockFile::LockOK; - if(!ret.success) { - lock->getLockInfo(ret.holderPid, ret.holderHostname, ret.holderApp); + + // Attempt to lock file, despite the possibility to do so and presence of the request (doLocking) + // This is required as KLockFile::getLockInfo() works only after KLockFile::lock() is called + ret.attemptRelock(); + if( ret.lockResult == KLockFile::LockOK ) { + // Unlock immediately if we shouldn't have locked it + if( !lockedDBus ) { + ret.lockFile->unlock(); + } + } else { + // If locking failed, retrieve the lock's metadata + ret.lockFile->getLockInfo( ret.holderPid, ret.holderHostname, ret.holderApp ); + + if( lockedDBus ) { + // Since the lock-file is secondary, try to force-lock it if D-Bus locking succeeded + ret.forceRemoveLockfile(); + ret.attemptRelock(); + + // Finally, if removing didn't help, cancel D-Bus locking altogether. + if( ret.lockResult != KLockFile::LockOK ) { + connection.unregisterService( service ); + // do not set lockedDBus to false: ret.success will be then set to + // lockedDBus, and we want it true to indicate that the D-Bus name is free + } + } } + + // Set the result by D-Bus status + ret.success = doLocking ? lockedDBus : canLockDBus; return ret; } // Internal helper class class SessionChooserDialog : public KDialog { Q_OBJECT public: SessionChooserDialog(QListView* view, QStandardItemModel* model); bool eventFilter(QObject* object, QEvent* event); public Q_SLOTS: void updateState(); void doubleClicked(QModelIndex); private Q_SLOTS: void deleteButtonPressed(); void showDeleteButton(); void itemEntered(const QModelIndex& index); private: QListView* m_view; QStandardItemModel* m_model; QTimer m_updateStateTimer; QPushButton* m_deleteButton; QTimer m_deleteButtonTimer; int m_deleteCandidateRow; }; SessionChooserDialog::SessionChooserDialog(QListView* view, QStandardItemModel* model) : m_view(view), m_model(model), m_deleteCandidateRow(-1) { m_updateStateTimer.setInterval(5000); m_updateStateTimer.setSingleShot(false); m_updateStateTimer.start(); connect(&m_updateStateTimer, SIGNAL(timeout()), SLOT(updateState())); connect(view, SIGNAL(doubleClicked(QModelIndex)), SLOT(doubleClicked(QModelIndex))); connect(view, SIGNAL(entered(QModelIndex)), SLOT(itemEntered(QModelIndex))); m_deleteButton = new KPushButton(view->viewport()); m_deleteButton->setIcon(KIcon("edit-delete")); m_deleteButton->setToolTip(i18nc("@info", "Delete session")); m_deleteButton->hide(); connect(m_deleteButton, SIGNAL(clicked(bool)), SLOT(deleteButtonPressed())); m_deleteButtonTimer.setInterval(500); m_deleteButtonTimer.setSingleShot(true); connect(&m_deleteButtonTimer, SIGNAL(timeout()), SLOT(showDeleteButton())); view->setMouseTracking(true); view->installEventFilter(this); } void SessionChooserDialog::doubleClicked(QModelIndex index) { QStandardItem* item = m_model->itemFromIndex(index); if(item && item->isEnabled()) accept(); } void SessionChooserDialog::updateState() { // Sometimes locking may take some time, so we stop the timer, to prevent an 'avalanche' of events m_updateStateTimer.stop(); for(int row = 0; row < m_model->rowCount(); ++row) { QString session = m_model->index(row, 0).data().toString(); if(session == i18n("Create New Session")) continue; QString state, tooltip; SessionController::LockSessionState lockState = KDevelop::SessionController::tryLockSession(session); if(!lockState) { tooltip = i18n("Active session.\npid %1, app %2, host %3", lockState.holderPid, lockState.holderApp, lockState.holderHostname); state = i18n("Running"); } if(m_model->item(row, 2)) { m_model->item(row, 1)->setIcon(lockState ? KIcon("") : KIcon("media-playback-start")); m_model->item(row, 1)->setToolTip(tooltip); m_model->item(row, 2)->setText(state); } } m_updateStateTimer.start(); } QString SessionController::showSessionChooserDialog(QString headerText, bool onlyRunning) { // The catalog hasn't been loaded yet KGlobal::locale()->insertCatalog("kdevplatform"); QListView* view = new QListView; QStandardItemModel* model = new QStandardItemModel(view); SessionChooserDialog dialog(view, model); view->setEditTriggers(QAbstractItemView::NoEditTriggers); QVBoxLayout layout(dialog.mainWidget()); if(!headerText.isEmpty()) layout.addWidget(new QLabel(headerText)); model->setColumnCount(3); model->setHeaderData(0, Qt::Horizontal,i18n("Identity")); model->setHeaderData(1, Qt::Horizontal, i18n("Contents")); model->setHeaderData(2, Qt::Horizontal,i18n("State")); view->setModel(model); view->setModelColumn(1); layout.addWidget(view); int row = 0; int defaultRow = 0; QString defaultSession = KGlobal::config()->group( cfgSessionGroup() ).readEntry( cfgActiveSessionEntry(), "default" ); foreach(const KDevelop::SessionInfo& si, KDevelop::SessionController::availableSessionInfo()) { if ( si.name.isEmpty() && si.projects.isEmpty() ) { continue; } bool running = KDevelop::SessionController::tryLockSession(si.uuid.toString()); if(onlyRunning && !running) continue; if(si.uuid.toString() == defaultSession) defaultRow = row; model->setItem(row, 0, new QStandardItem(si.uuid.toString())); model->setItem(row, 1, new QStandardItem(si.description)); model->setItem(row, 2, new QStandardItem); if(defaultRow == row && running) ++defaultRow; ++row; } int cnsRow = row; if(!onlyRunning) { model->setItem(row, 0, new QStandardItem); model->setItem(row, 1, new QStandardItem(KIcon("window-new"), i18n("Create New Session"))); } dialog.updateState(); dialog.mainWidget()->layout()->setContentsMargins(0,0,0,0); view->selectionModel()->setCurrentIndex(model->index(defaultRow, 0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); view->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); ///@todo We need a way to get a proper size-hint from the view, but unfortunately, that only seems possible after the view was shown. dialog.setInitialSize(QSize(900, 600)); if(dialog.exec() != QDialog::Accepted) { return QString(); } QModelIndex selected = view->selectionModel()->currentIndex(); if(selected.isValid()) { QString ret; if( selected.row() == cnsRow ) { qsrand(QDateTime::currentDateTime().toTime_t()); ret = QUuid::createUuid().toString(); } else { selected = selected.sibling(selected.row(), 0); ret = selected.data().toString(); } return ret; } return QString(); } +QString SessionController::handleLockedSession( const QString& sessionName, const QString& sessionId, + const SessionController::LockSessionState& state ) +{ + if( state ) { + return sessionId; + } + + // Set to true if lock-file is not free + bool lockFileOwned = state.lockResult != KLockFile::LockOK; + + // Set the locking problem description. + QString problemDescription; + if( state.success ) { + // Do not try to call D-Bus if the D-Bus service is free (unregistered). + switch( state.lockResult ) { + case KLockFile::LockStale: + problemDescription = i18nc("@info", "The session lock-file is stale."); + break; + + case KLockFile::LockError: + problemDescription = i18nc("@info", "Error while taking the session lock-file."); + break; + + case KLockFile::LockFail: + problemDescription = i18nc("@info", "The session lock-file is owned."); + break; + + case KLockFile::LockOK: + default: + // We shouldn't have both D-Bus service name and lockfile free by now. + Q_ASSERT( false ); + break; + } + } else { + QDBusInterface interface(state.DBusService, + "/kdevelop/MainWindow", "org.kdevelop.MainWindow", + QDBusConnection::sessionBus()); + if (interface.isValid()) { + QDBusReply reply = interface.call("ensureVisible"); + if (reply.isValid()) { + kDebug() << i18nc("@info:shell", "made running %1 instance (PID: %2) visible", state.holderApp, state.holderPid); + return QString(); + } + } + + problemDescription = i18nc("@info", + "The given application did not respond to a DBUS call, " + "it may have crashed or is hanging."); + } + + QString problemHeader; + if( lockFileOwned ) { + problemHeader = i18nc("@info", "Failed to lock the session %1, " + "already locked by %2 on %3 (PID %4).", + sessionName, state.holderApp, state.holderHostname, state.holderPid); + } else { + problemHeader = i18nc("@info", "Failed to lock the session %1 (lock-file unavailable).", + sessionName); + } + + QString problemResolution; + if( state.success ) { + problemResolution = i18nc("@info", + "

Do you want to remove the lock file and force a new %1 instance?
" + "Beware: Only do this if you are sure there is no running" + " process using this session.

" + "

Otherwise, close the offending application instance " + "or choose another session to launch.

", + KCmdLineArgs::aboutData()->programName()); + } else { + problemResolution = i18nc("@info", + "

Please, close the offending application instance " + "or choose another session to launch.

"); + } + + QString errmsg = "

" + + problemHeader + + "
" + + problemDescription + + "

" + + problemResolution; + + KGuiItem retry = KStandardGuiItem::cont(); + if( state.success ) { + retry.setText(i18nc("@action:button", "Remove lock file")); + } else { + retry.setText(i18nc("@action:button", "Retry startup")); + } + KGuiItem choose = KStandardGuiItem::configure(); + choose.setText(i18nc("@action:button", "Choose another session")); + KGuiItem cancel = KStandardGuiItem::quit(); + int ret = KMessageBox::warningYesNoCancel(0, errmsg, i18nc("@title:window", "Failed to Lock Session %1", sessionName), + retry, choose, cancel); + switch( ret ) { + case KMessageBox::Yes: + if( state.success ) { + state.forceRemoveLockfile(); + } + return sessionId; + break; + + case KMessageBox::No: { + QString errmsg = i18nc("@info", "The session %1 is already active in another running instance.", + sessionName); + return showSessionChooserDialog(errmsg); + break; + } + + case KMessageBox::Cancel: + default: + return QString(); + break; + } +} + void SessionChooserDialog::itemEntered(const QModelIndex& index) { // The last row says "Create new session", we don't want to delete that if(index.row() == m_model->rowCount()-1) { m_deleteButton->hide(); m_deleteButtonTimer.stop(); return; } // align the delete-button to stay on the right border of the item // we need the right most column's index QModelIndex in = m_model->index( index.row(), 1 ); const QRect rect = m_view->visualRect(in); m_deleteButton->resize(rect.height(), rect.height()); QPoint p(rect.right() - m_deleteButton->size().width(), rect.top()+rect.height()/2-m_deleteButton->height()/2); m_deleteButton->move(p); m_deleteCandidateRow = index.row(); m_deleteButtonTimer.start(); } void SessionChooserDialog::showDeleteButton() { m_deleteButton->show(); } bool SessionChooserDialog::eventFilter(QObject* object, QEvent* event) { if(object == m_view && event->type() == QEvent::Leave ) { m_deleteButtonTimer.stop(); m_deleteButton->hide(); } return false; } void SessionChooserDialog::deleteButtonPressed() { if(m_deleteCandidateRow == -1) return; const QString text = i18nc("@info", "The session and all contained settings will be deleted. The projects will stay unaffected. Do you really want to continue?"); const QString caption = i18nc("@title", "Delete Session"); const KGuiItem deleteItem(i18nc("@action:button", "Delete"), KIcon("edit-delete")); const KGuiItem cancelItem(i18nc("@action:button", "Cancel"), KIcon("dialog-cancel")); if(KMessageBox::warningYesNo(this, text, caption, deleteItem, cancelItem) == KMessageBox::Yes) { QModelIndex index = m_model->index(m_deleteCandidateRow, 0); QStandardItem *item = m_model->itemFromIndex(index); const QString uuid = item->text(); //FIXME: What about running sessions? KDevelop::Session session( uuid ); session.deleteFromDisk(); m_model->removeRows( m_deleteCandidateRow, 1 ); m_deleteCandidateRow = -1; } } +QString SessionController::sessionDir() +{ + if( !activeSession() ) + return QString(); + return d->ownSessionDirectory(); +} QString SessionController::sessionName() { if(!activeSession()) return QString(); return activeSession()->description(); } } #include "sessioncontroller.moc" #include "moc_sessioncontroller.cpp" diff --git a/shell/sessioncontroller.h b/shell/sessioncontroller.h index 551894d413..ee0c8ced40 100644 --- a/shell/sessioncontroller.h +++ b/shell/sessioncontroller.h @@ -1,143 +1,174 @@ /* This file is part of KDevelop Copyright 2008 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef SESSIONCONTROLLER_H #define SESSIONCONTROLLER_H #include "shellexport.h" #include #include #include #include #include #include #include #include #include #include #include namespace KDevelop { class Session; class ISession; struct SessionInfo { QString name; QUuid uuid; QString description; KUrl::List projects; }; class KDEVPLATFORMSHELL_EXPORT SessionController : public QObject, public KXMLGUIClient { Q_OBJECT public: SessionController( QObject *parent = 0 ); virtual ~SessionController(); void initialize( const QString& session ); void cleanup(); struct LockSessionState { LockSessionState() : success(true), - holderPid(-1) + holderPid(-1), + lockResult( KLockFile::LockOK ) { } operator bool() const { - return success; + return success && ( lockResult == KLockFile::LockOK ); } bool success; QString holderApp; QString holderHostname; int holderPid; - QString lockFile; + QString lockFilename; + KLockFile::Ptr lockFile; + KLockFile::LockResult lockResult; + QString sessionId; + QString DBusService; + + /// Tries to own the lock-file; stores its status in @ref lockResult + void attemptRelock() + { + lockResult = lockFile->lock( KLockFile::ForceFlag | KLockFile::NoBlockFlag ); + } + + /// Force-removes the lock-file. + void forceRemoveLockfile() const + { + if( QFile::exists( lockFilename ) ) { + QFile::remove( lockFilename ); + } + } }; - - /// Returns whether the given session can be locked - static LockSessionState tryLockSession(QString id); - + + /// Returns whether the given session can be locked (i. e., is not locked currently). + /// @param doLocking whether to really lock the session or just "dry-run" the locking process + static LockSessionState tryLockSession(QString id, bool doLocking = false); + /// The application should call this on startup to tell the /// session-controller about the received arguments. /// Some of them may need to be passed to newly opened sessions. static void setArguments(int argc, char** argv); - - bool lockSession(); ///Finds a session by its name or by its UUID Session* session( const QString& nameOrId ) const; virtual ISession* activeSession() const; QList sessionNames() const; Session* createSession( const QString& name ); QList sessions() const; void loadDefaultSession( const QString& session ); void startNewSession(); void loadSession( const QString& nameOrId ); void deleteSession( const QString& nameOrId ); QString cloneSession( const QString& nameOrid ); static QString sessionDirectory(); static QString cfgSessionGroup(); static QString cfgActiveSessionEntry(); static QList< SessionInfo > availableSessionInfo(); /// Shows a dialog where the user can choose the session /// @param headerText an additional text that will be shown at the top in a label /// @param onlyRunning whether only currently running sessions should be shown static QString showSessionChooserDialog(QString headerText = QString(), bool onlyRunning = false); - + + /// Should be called if session to be opened is locked. + /// It attempts to bring existing instance's window up via a DBus call; if that succeeds, empty string is returned. + /// Otherwise (if the app did not respond) it shows a dialog where the user may choose + /// 1) to force-remove the lockfile and continue, + /// 2) to select another session via \ref showSessionChooserDialog, + /// 3) to quit the current (starting-up) instance. + /// @param sessionName session name (for the message) + /// @param sessionId current session GUID (to return if user chooses force-removal) + /// @param state session lock state + /// @return new session GUID to try or an empty string if application startup shall be aborted + static QString handleLockedSession( const QString& sessionName, const QString& sessionId, + const SessionController::LockSessionState& state ); + void plugActions(); void emitQuitSession() { emit quitSession(); } public Q_SLOTS: // Returns the pretty name of the currently active session (used in the shell integration) virtual Q_SCRIPTABLE QString sessionName(); // Returns the directory associated to the active session (used in the shell integration) virtual Q_SCRIPTABLE QString sessionDir(); Q_SIGNALS: void sessionLoaded( ISession* ); void sessionDeleted( const QString& ); void quitSession(); private slots: void updateSessionDescriptions(); private: Q_PRIVATE_SLOT( d, void newSession() ) Q_PRIVATE_SLOT( d, void configureSessions() ) Q_PRIVATE_SLOT( d, void deleteSession() ) Q_PRIVATE_SLOT( d, void renameSession() ) Q_PRIVATE_SLOT( d, void loadSessionFromAction( QAction* ) ) class SessionControllerPrivate* const d; }; } #endif