[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-games-devel
Subject:    [Kde-games-devel] KHighscore locking mechanism
From:       Nicolas Hadacek <hadacek () kde ! org>
Date:       2003-05-05 21:39:33
[Download RAW message or body]

Hi all,

I am currently trying to solve the problems of using global (for a computer) 
config files for saving highscores. Some basic mechanism is already in place 
in KHighscore but a big (potential) problem is race condition between games 
played by several users at the time when updating the highscores lists.

So I propose a locking mechanism (see the patch against khighscore.h/cpp and a 
separate kfilelock.h/cpp which could be used in other parts of KDE). These 
changes are untested but I just want to have some opinions :)

The second step is to use the locking in KExtHighscore which should be quite 
easy (I have made the changes at home) since support for multiple user is 
there (but basically untested).

The third step is to provide some upgrade path from highscore list per user to 
a global highscore list... I can do that later :)

Flames, praises, comments, ... welcome !

see you,
Nicolas

["highscores.diff" (text/x-diff)]

Index: khighscore.h
===================================================================
RCS file: /home/kde/kdegames/libkdegames/highscore/khighscore.h,v
retrieving revision 1.7
diff -u -2 -d -p -b -r1.7 khighscore.h
--- khighscore.h	6 Jan 2002 18:05:53 -0000	1.7
+++ khighscore.h	6 May 2003 03:26:53 -0000
@@ -81,5 +81,47 @@ class KHighscore : public QObject
 	Q_OBJECT
 public:
+        /**
+         * Constructor.
+         */
 	KHighscore(QObject* parent = 0);
+
+        /**
+         * Read the current state of the config file. Remember that if not
+         * locked for writing, the config file can change at any time.
+         * (This method is only useful for a global config file).
+         */
+        void readCurrentConfig();
+
+        /**
+         * Lock the config file for writing (does nothing and return true if a
+         * local config is used). Perform writing without GUI
+         * interaction to avoid blocking and don't forget to unlock the file
+         * as soon as possible with @ref unlock().
+         *
+         * If the config file cannot be locked,
+         * the method waits for 1 second and if it failed again displays
+         * a message box asking for retry or cancel.
+         * @param widget used as parent of the message box.
+         * @param display if false do not display the message box.
+         *
+         * @return false on error or if the config file is locked by another
+         * process. In such case, the config file is set to read-only.
+         */
+        bool lockForWriting(QWidget *widget = 0, bool display = true);
+
+        /**
+         * Unlock the config file. @see lockForWriting.
+         */
+        void unlock();
+
+        /** @return if the config file is locked (return true if a local config
+         * file is used.
+         */
+        bool isLocked() const;
+
+        /**
+         * Destructor.
+         * Remove the eventual lock on the config file.
+         */
 	~KHighscore();
 
@@ -219,4 +261,9 @@ protected:
 	 **/
 	KConfig* config() const;
+
+        /**
+         * @return the config filename if a global config file is used.
+         */
+        QString globalConfigFilename() const;
 
 private:
Index: khighscore.cpp
===================================================================
RCS file: /home/kde/kdegames/libkdegames/highscore/khighscore.cpp,v
retrieving revision 1.15
diff -u -2 -d -p -b -r1.15 khighscore.cpp
--- khighscore.cpp	25 Feb 2003 01:03:18 -0000	1.15
+++ khighscore.cpp	6 May 2003 03:26:53 -0000
@@ -21,7 +21,12 @@
 */
 
+#include <unistd.h>
+
 #include <kapplication.h>
 #include <ksimpleconfig.h>
 #include <kglobal.h>
+#include <kstdguiitem.h>
+#include <klocale.h>
+#include <kmessagebox.h>
 
 #include "khighscore.h"
@@ -33,8 +38,5 @@ class KHighscorePrivate
 {
 public:
-	KHighscorePrivate()
-	{
-		mConfig = 0;
-	}
+    KHighscorePrivate() : mConfig(0) {}
 
 	KConfig* mConfig;
@@ -45,35 +47,94 @@ KHighscore::KHighscore(QObject* parent) 
 {
  d = new KHighscorePrivate;
+    readCurrentConfig();
+}
 
+void KHighscore::readCurrentConfig()
+{
+#ifdef HIGHSCORE_DIRECTORY
+    delete d->mConfig;
+    d->mConfig = new KSimpleConfig(globalConfigFilename(), true);
+#endif
 }
 
-KHighscore::~KHighscore()
+bool KHighscore::lockForWriting(QWidget *widget, bool display)
 {
-// not necessary, as KConfig destructor should handle this
- sync();
- if (d->mConfig) {
+#ifdef HIGHSCORE_DIRECTORY
+    if ( isLocked() ) return true; // already locked
+    bool first = true;
+    QString filename = globalConfigFile();
+    for (;;) {
+        bool ok = ( FileLock::lock(filename)==0 );
+        if (ok) {
 	delete d->mConfig;
+            d->mConfig = new KSimpleConfig(filename, false);
+            return true;
  }
- delete d;
+        if ( !first ) {
+            if ( !display ) break;
+            KGuiItem item = KStdGuiItem::cont();
+            item.setText(i18n("Retry"));
+            int res = KMessageBox::warningContinueCancel(parent, i18n("Cannot lock \
the global config file. Another process is probably currently writing to it."), \
i18n("Config file locked"), item, "ask_lock_game_config_file"); +            if ( \
res==KMessageBox::Cancel ) break; +        } else sleep(1);
+        first = false;
+    }
+    return false;
+#else
+    Q_UNUSED(widget);
+    Q_UNUSED(display);
+    return true;
+#endif
 }
 
-KConfig* KHighscore::config() const
+void KHighscore::unlock()
 {
- #ifdef HIGHSCORE_DIRECTORY
-	if (!d->mConfig) {
+#ifdef HIGHSCORE_DIRECTORY
+    if ( isLocked () ) FileLock::unlock(globalConfigFilename());
+    d->mConfig->setReadOnly(true);
+#endif
+}
+
+bool KHighscore::isLocked() const
+{
+#ifdef HIGHSCORE_DIRECTORY
+    return !d->mConfig->isReadOnly();
+#else
+    return true;
+#endif
+}
+
+QString KHighscore::globalConfigFilename() const
+{
+#ifdef HIGHSCORE_DIRECTORY
 		//AB: is instanceName() correct? MUST be the same for all
 		//processes of the game!
-		QString file=QString::fromLocal8Bit("%1/%2").arg(HIGHSCORE_DIRECTORY).arg(KGlobal::instance()->instanceName());
                
-		d->mConfig = new KSimpleConfig(file);
-	}
+    return QString::fromLocal8Bit("%1/%2").arg(HIGHSCORE_DIRECTORY)
+        .arg(KGlobal::instance()->instanceName());
+#else
+    return QString::null;
+#endif
+}
+
+KHighscore::~KHighscore()
+{
+#ifdef HIGHSCORE_DIRECTORY
+    unlock();
+    delete d->mConfig;
+#endif
+    delete d;
+}
+
+KConfig* KHighscore::config() const
+{
+#ifdef HIGHSCORE_DIRECTORY
 	return d->mConfig;
- #else
+#else
 	return kapp->config();
- #endif
+#endif
 }
 
 void KHighscore::writeEntry(int entry, const QString& key, const QVariant& value)
 {
- // save the group in case that we are on kapp->config()
  KConfigGroupSaver cg(config(), group());
  QString confKey = QString("%1_%2").arg(entry).arg(key);
@@ -83,5 +144,4 @@ void KHighscore::writeEntry(int entry, c
 void KHighscore::writeEntry(int entry, const QString& key, int value)
 {
- // save the group in case that we are on kapp->config()
  KConfigGroupSaver cg(config(), group());
  QString confKey = QString("%1_%2").arg(entry).arg(key);


["kfilelock.cpp" (text/x-c++src)]

#include "kfilelock.h"

#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <errno.h>

#include <qfileinfo.h>


// this code is heavily borrowed from gjots
// Copyright (C) 2002 Robert Hepple

int FileLock::lock(const QString &filename)
{
    QFileInfo info(filename);
    QString lockFilename = info.dirPath() + "/.#" + info.fileName();

    int fd = open(lockFilename.local8Bit(), O_RDWR | O_CREAT, 0644);
    if ( fd<0 ) return -1;
    // be sure that no other process access the file
    if ( flock(fd, LOCK_EX | LOCK_NB)<0 ) {
        close(fd);
        return -1;
    }
    FILE *f = fdopen(fd, "r+");
    if ( f==0 ) {
        flock(fd, LOCK_UN);
        close(fd);
        return 1;
    }
    // look at the pid number in the file
    // and check if there is still a process with this pid
    pid_t pid;
    if ( fscanf(f, "%d", &pid)==1 && ( getpgid(pid)>=0 || errno!=ESRCH ) ) {
        flock(fd, LOCK_UN);
        fclose(f);
        return pid;
    }

    // write the process pid
    fseek(f, 0L, SEEK_SET);
    fprintf(f, "%d", getpid());

    flock(fd, LOCK_UN);
    fclose(f);
    return 0;
}

void FileLock::unlock(const QString &filename)
{
    QFileInfo info(filename);
    QString lockFilename = info.dirPath() + "/.#" + info.fileName();

    // first, make sure we locked this file
    int fd = open(lockFilename.local8Bit(), O_RDONLY);
    if ( fd<0 ) return;
    // be sure that no other process access the file
    if ( flock(fd, LOCK_EX | LOCK_NB)<0 ) {
        close(fd);
        return;
    }
    FILE *f = fdopen(fd, "r");
    if ( f==0 ) {
        flock(fd, LOCK_UN);
        close(fd);
        return;
    }
    // read the pid inside the file and check that it is ours
    pid_t pid;
    if ( fscanf(f, "%d", &pid)==1 && pid==getpid() )
        unlink(lockFilename.local8Bit());

    flock(fd, LOCK_UN);
    fclose(f);
}

["kfilelock.h" (text/x-chdr)]

#ifndef KFILELOCK_H
#define KFILELOCK_H

#include <qstring.h>


class FileLock
{
 public:
    /**
     * Create a file named ".#filename" and put the current
     * process id inside the file.
     * @return 0 on success, -1 on failure (no permission) or the pid number
     * of the process which currently holds the lock.
     */
    static int lock(const QString &filename);

    /**
     * Remove the file created by @ref lockFile.
     */
    static void unlock(const QString &filename);
};


#endif


_______________________________________________
kde-games-devel mailing list
kde-games-devel@mail.kde.org
http://mail.kde.org/mailman/listinfo/kde-games-devel


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic